From 5d2a82edb223035bd0d3a7312b7823052f98f9e4 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 Jul 2026 10:25:36 +0200 Subject: [PATCH 01/15] spec: add multiroot session support (AHP 0.6.0) - Add `MultipleWorkspaceFoldersCapability` and `AgentCapabilities.multipleWorkspaceFolders` to `types/channels-root/state.ts`, mirroring the `multipleChats` pattern. Presence signals the agent supports multiple equal-peer working directories. - Add `CreateSessionParams.workingDirectories` (plural, replaces the singular `workingDirectory`) to `types/channels-session/commands.ts`. The old singular field is retained as a deprecated backwards-compatible shorthand. Forked sessions ignore `workingDirectories` and inherit from the source session. - Add `AddWorkspaceFolderParams`, `RemoveWorkspaceFolderParams`, `WorkspaceFolderResult`, `AddWorkspaceFolderResult`, and `RemoveWorkspaceFolderResult` to `types/channels-session/commands.ts`. Both commands return the full directory set after the mutation. `removeWorkspaceFolder` is modelled as idempotent reconfigure-to-reduced-set (no server-side atomic remove primitive). - Register `addWorkspaceFolder` and `removeWorkspaceFolder` in `CommandMap` in `types/common/messages.ts` and `_ExpectedCommands` in `types/version/message-checks.ts`. - Version-gating: new commands and capability land in AHP 0.6.0 (a 0.x minor bump per the versioning doc, matching a capability boundary). Gating is purely via the `multipleWorkspaceFolders` capability + version handshake. The `ACTION_INTRODUCED_IN` / `NOTIFICATION_INTRODUCED_IN` maps are not touched because neither new symbols are state actions nor notifications. - Update `CHANGELOG.md` (spec and all five clients) with `## [Unreleased]` entries under `### Added` / `### Deprecated`. - Update `docs/guide/state-model.md` with a new "Multiroot Sessions" section plus updated `SessionState` / `SessionSummary` snippets. - Regenerate all client mirrors (Swift, Rust, Kotlin, TypeScript, Go) and update the four generator scripts (`generate-swift.ts`, `generate-rust.ts`, `generate-kotlin.ts`, `generate-go.ts`) with the new type registrations and helper methods. All 312 tests pass; 100% branch coverage on reducers.ts maintained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 27 +++++ clients/go/CHANGELOG.md | 4 + clients/go/ahptypes/commands.generated.go | 69 +++++++++++- clients/go/ahptypes/state.generated.go | 12 ++ clients/kotlin/CHANGELOG.md | 4 + .../generated/Commands.generated.kt | 62 ++++++++++- .../generated/Messages.generated.kt | 6 + .../generated/State.generated.kt | 13 ++- clients/rust/CHANGELOG.md | 4 + clients/rust/crates/ahp-types/src/commands.rs | 78 ++++++++++++- clients/rust/crates/ahp-types/src/state.rs | 14 +++ .../Generated/Commands.generated.swift | 79 +++++++++++++- .../Generated/Messages.generated.swift | 8 ++ .../Generated/State.generated.swift | 18 ++- clients/swift/CHANGELOG.md | 4 + clients/typescript/CHANGELOG.md | 4 + docs/guide/state-model.md | 53 ++++++++- schema/actions.schema.json | 9 ++ schema/commands.schema.json | 103 +++++++++++++++++- schema/errors.schema.json | 103 +++++++++++++++++- schema/notifications.schema.json | 9 ++ schema/state.schema.json | 9 ++ scripts/generate-go.ts | 3 + scripts/generate-kotlin.ts | 9 ++ scripts/generate-rust.ts | 3 + scripts/generate-swift.ts | 11 ++ types/channels-root/state.ts | 17 +++ types/channels-session/commands.ts | 91 +++++++++++++++- types/common/messages.ts | 6 + types/version/message-checks.ts | 2 + 30 files changed, 820 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf85ddae..45723c478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,33 @@ changes accumulate. Track in-flight protocol changes via PRs touching Spec version: `0.6.0` +### Added + +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, gating + first-class multiroot session support. When absent, clients MUST NOT call + `addWorkspaceFolder` / `removeWorkspaceFolder` and MUST NOT set more than one + entry in `CreateSessionParams.workingDirectories`. +- `CreateSessionParams.workingDirectories` — the set of working directories the + session's agent is granted tool access to. All entries are equal peers with no + privileged "primary". Ignored for forked sessions (a fork inherits its + working directories from the source session). +- `addWorkspaceFolder` command — grants a running session's agent tool access to + an additional working directory. Returns `AddWorkspaceFolderResult` carrying + the full directory set after the mutation. +- `removeWorkspaceFolder` command — revokes tool access to one working directory + by reconfiguring the agent to the reduced set. There is no atomic server-side + "remove one" primitive; the command is idempotent (removing a directory not in + the set is a no-op that still returns the current set). Returns + `RemoveWorkspaceFolderResult` carrying the full directory set after the + mutation. + +### Deprecated + +- `CreateSessionParams.workingDirectory` (singular) in favour of + `workingDirectories`. Retained as a single-directory shorthand for backwards + compatibility; servers MUST still honour it when `workingDirectories` is + absent. + ## [0.5.2] — 2026-07-09 Spec version: `0.5.2` diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index 6ac6b314e..3c702b5d3 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -14,6 +14,10 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] +### Added + +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). + ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 38e2991f2..5b767cd75 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -305,8 +305,20 @@ type CreateSessionParams struct { Channel URI `json:"channel"` // Agent provider ID Provider *string `json:"provider,omitempty"` - // Working directory for the session - WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // The working directories the session's agent is granted tool access to. + // A session may span multiple directories, all of which are equal peers — + // there is no privileged "primary" directory. + // + // A client MUST NOT supply more than one entry unless the agent advertises + // {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + // capability treats only the first entry as the session's working directory + // and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` + // to change the set after the session has started. + // + // Ignored for forked sessions — a fork inherits its working directories + // from the source session identified by `fork`. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` + WorkingDirectory *URI `json:"workingDirectory,omitempty"` // Fork from an existing session. The new session is populated with content // from the source session up to and including the specified turn's response. Fork *SessionForkSource `json:"fork,omitempty"` @@ -341,6 +353,59 @@ type DisposeSessionParams struct { Channel URI `json:"channel"` } +// Grants the session's agent tool access to a working directory, adding it to +// the session's set of {@link CreateSessionParams.workingDirectories | working +// directories}. Only valid when the agent advertises +// `multipleWorkspaceFolders`; servers MUST reject this command otherwise. +// +// All directories in the set are equal peers — there is no privileged +// "primary". Adding a directory that is already in the set is a no-op that +// still returns the current full set. +type AddWorkspaceFolderParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Directory to grant tool access to. + Folder URI `json:"folder"` +} + +// Revokes the session's agent tool access to one of its working directories. +// There is no server-side primitive to "remove" a single directory mid-session +// — the server instead reconfigures the agent with the reduced directory set +// and returns it, so this command is safe to model as idempotent: removing a +// directory that is not in the set is a no-op that still returns the current +// full set. +// +// A server MAY refuse to remove a directory it cannot relinquish while the +// session is live (for example one bound to the running agent process), +// returning an error rather than a result. The protocol itself designates no +// directory as special. +type RemoveWorkspaceFolderParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Directory to revoke tool access to. + Folder URI `json:"folder"` +} + +// Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder` +// commands: the session's full set of working directories after the mutation. +// All entries are equal peers; the order carries no meaning. +type WorkspaceFolderResult struct { + // The session's working directories after the mutation. + Directories []URI `json:"directories"` +} + +// Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}. +type AddWorkspaceFolderResult struct { + // The session's working directories after the mutation. + Directories []URI `json:"directories"` +} + +// Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}. +type RemoveWorkspaceFolderResult struct { + // The session's working directories after the mutation. + Directories []URI `json:"directories"` +} + // Identifies a source chat and turn to fork from. type ChatForkSource struct { // URI of the existing chat to fork from diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 462087f38..1324c33d1 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -563,6 +563,12 @@ type AgentCapabilities struct { // session starts with. An empty object `{}` advertises multi-chat without // forking; set {@link MultipleChatsCapability.fork} to also allow forking. MultipleChats *MultipleChatsCapability `json:"multipleChats,omitempty"` + // The session's agent can be granted tool access to more than one working + // directory, with all directories treated as equal peers (no primary). When + // absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` + // and MUST NOT set more than one entry in + // {@link CreateSessionParams.workingDirectories}. + MultipleWorkspaceFolders *MultipleWorkspaceFoldersCapability `json:"multipleWorkspaceFolders,omitempty"` } // Options for the {@link AgentCapabilities.multipleChats} capability. @@ -573,6 +579,12 @@ type MultipleChatsCapability struct { Fork *bool `json:"fork,omitempty"` } +// Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. +// Currently carries no sub-options; presence of an empty object `{}` is the +// entire signal. +type MultipleWorkspaceFoldersCapability struct { +} + type SessionModelInfo struct { // Model identifier Id string `json:"id"` diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 831496149..321786c52 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -15,6 +15,10 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump ## [Unreleased] +### Added + +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). + ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 5855cb8b8..d6676a344 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -369,8 +369,20 @@ data class CreateSessionParams( */ val provider: String? = null, /** - * Working directory for the session + * The working directories the session's agent is granted tool access to. + * A session may span multiple directories, all of which are equal peers — + * there is no privileged "primary" directory. + * + * A client MUST NOT supply more than one entry unless the agent advertises + * {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + * capability treats only the first entry as the session's working directory + * and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` + * to change the set after the session has started. + * + * Ignored for forked sessions — a fork inherits its working directories + * from the source session identified by `fork`. */ + val workingDirectories: List? = null, val workingDirectory: String? = null, /** * Fork from an existing session. The new session is populated with content @@ -414,6 +426,54 @@ data class DisposeSessionParams( val channel: String ) +@Serializable +data class AddWorkspaceFolderParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Directory to grant tool access to. + */ + val folder: String +) + +@Serializable +data class RemoveWorkspaceFolderParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Directory to revoke tool access to. + */ + val folder: String +) + +@Serializable +data class WorkspaceFolderResult( + /** + * The session's working directories after the mutation. + */ + val directories: List +) + +@Serializable +data class AddWorkspaceFolderResult( + /** + * The session's working directories after the mutation. + */ + val directories: List +) + +@Serializable +data class RemoveWorkspaceFolderResult( + /** + * The session's working directories after the mutation. + */ + val directories: List +) + @Serializable data class ChatForkSource( /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt index 07ed4755c..22033a54a 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt @@ -98,6 +98,12 @@ object AhpCommands { fun disposeSession(id: Long, params: DisposeSessionParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "disposeSession", params = params) + fun addWorkspaceFolder(id: Long, params: AddWorkspaceFolderParams): JsonRpcRequest = + JsonRpcRequest(id = id, method = "addWorkspaceFolder", params = params) + + fun removeWorkspaceFolder(id: Long, params: RemoveWorkspaceFolderParams): JsonRpcRequest = + JsonRpcRequest(id = id, method = "removeWorkspaceFolder", params = params) + fun listSessions(id: Long, params: ListSessionsParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "listSessions", params = params) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 9a008418f..603482fcc 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -920,7 +920,15 @@ data class AgentCapabilities( * session starts with. An empty object `{}` advertises multi-chat without * forking; set {@link MultipleChatsCapability.fork} to also allow forking. */ - val multipleChats: MultipleChatsCapability? = null + val multipleChats: MultipleChatsCapability? = null, + /** + * The session's agent can be granted tool access to more than one working + * directory, with all directories treated as equal peers (no primary). When + * absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` + * and MUST NOT set more than one entry in + * {@link CreateSessionParams.workingDirectories}. + */ + val multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? = null ) @Serializable @@ -933,6 +941,9 @@ data class MultipleChatsCapability( val fork: Boolean? = null ) +@Serializable +class MultipleWorkspaceFoldersCapability + @Serializable data class SessionModelInfo( /** diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 0d80fff39..592ae5ad5 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -15,6 +15,10 @@ matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] +### Added + +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). + ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index a9f6ef3ff..9e40355b9 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -391,7 +391,20 @@ pub struct CreateSessionParams { /// Agent provider ID #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, - /// Working directory for the session + /// The working directories the session's agent is granted tool access to. + /// A session may span multiple directories, all of which are equal peers — + /// there is no privileged "primary" directory. + /// + /// A client MUST NOT supply more than one entry unless the agent advertises + /// {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + /// capability treats only the first entry as the session's working directory + /// and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` + /// to change the set after the session has started. + /// + /// Ignored for forked sessions — a fork inherits its working directories + /// from the source session identified by `fork`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, /// Fork from an existing session. The new session is populated with content @@ -434,6 +447,69 @@ pub struct DisposeSessionParams { pub channel: Uri, } +/// Grants the session's agent tool access to a working directory, adding it to +/// the session's set of {@link CreateSessionParams.workingDirectories | working +/// directories}. Only valid when the agent advertises +/// `multipleWorkspaceFolders`; servers MUST reject this command otherwise. +/// +/// All directories in the set are equal peers — there is no privileged +/// "primary". Adding a directory that is already in the set is a no-op that +/// still returns the current full set. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddWorkspaceFolderParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Directory to grant tool access to. + pub folder: Uri, +} + +/// Revokes the session's agent tool access to one of its working directories. +/// There is no server-side primitive to "remove" a single directory mid-session +/// — the server instead reconfigures the agent with the reduced directory set +/// and returns it, so this command is safe to model as idempotent: removing a +/// directory that is not in the set is a no-op that still returns the current +/// full set. +/// +/// A server MAY refuse to remove a directory it cannot relinquish while the +/// session is live (for example one bound to the running agent process), +/// returning an error rather than a result. The protocol itself designates no +/// directory as special. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveWorkspaceFolderParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Directory to revoke tool access to. + pub folder: Uri, +} + +/// Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder` +/// commands: the session's full set of working directories after the mutation. +/// All entries are equal peers; the order carries no meaning. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceFolderResult { + /// The session's working directories after the mutation. + pub directories: Vec, +} + +/// Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddWorkspaceFolderResult { + /// The session's working directories after the mutation. + pub directories: Vec, +} + +/// Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveWorkspaceFolderResult { + /// The session's working directories after the mutation. + pub directories: Vec, +} + /// Identifies a source chat and turn to fork from. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 3c857eb6a..e9cd52746 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -779,6 +779,13 @@ pub struct AgentCapabilities { /// forking; set {@link MultipleChatsCapability.fork} to also allow forking. #[serde(default, skip_serializing_if = "Option::is_none")] pub multiple_chats: Option, + /// The session's agent can be granted tool access to more than one working + /// directory, with all directories treated as equal peers (no primary). When + /// absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` + /// and MUST NOT set more than one entry in + /// {@link CreateSessionParams.workingDirectories}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub multiple_workspace_folders: Option, } /// Options for the {@link AgentCapabilities.multipleChats} capability. @@ -792,6 +799,13 @@ pub struct MultipleChatsCapability { pub fork: Option, } +/// Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. +/// Currently carries no sub-options; presence of an empty object `{}` is the +/// entire signal. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MultipleWorkspaceFoldersCapability {} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelInfo { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index a31740724..20f4d8c8d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -354,7 +354,19 @@ public struct CreateSessionParams: Codable, Sendable { public var channel: String /// Agent provider ID public var provider: String? - /// Working directory for the session + /// The working directories the session's agent is granted tool access to. + /// A session may span multiple directories, all of which are equal peers — + /// there is no privileged "primary" directory. + /// + /// A client MUST NOT supply more than one entry unless the agent advertises + /// {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + /// capability treats only the first entry as the session's working directory + /// and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` + /// to change the set after the session has started. + /// + /// Ignored for forked sessions — a fork inherits its working directories + /// from the source session identified by `fork`. + public var workingDirectories: [String]? public var workingDirectory: String? /// Fork from an existing session. The new session is populated with content /// from the source session up to and including the specified turn's response. @@ -384,6 +396,7 @@ public struct CreateSessionParams: Codable, Sendable { public init( channel: String, provider: String? = nil, + workingDirectories: [String]? = nil, workingDirectory: String? = nil, fork: SessionForkSource? = nil, config: [String: AnyCodable]? = nil, @@ -392,6 +405,7 @@ public struct CreateSessionParams: Codable, Sendable { ) { self.channel = channel self.provider = provider + self.workingDirectories = workingDirectories self.workingDirectory = workingDirectory self.fork = fork self.config = config @@ -411,6 +425,69 @@ public struct DisposeSessionParams: Codable, Sendable { } } +public struct AddWorkspaceFolderParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Directory to grant tool access to. + public var folder: String + + public init( + channel: String, + folder: String + ) { + self.channel = channel + self.folder = folder + } +} + +public struct RemoveWorkspaceFolderParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Directory to revoke tool access to. + public var folder: String + + public init( + channel: String, + folder: String + ) { + self.channel = channel + self.folder = folder + } +} + +public struct WorkspaceFolderResult: Codable, Sendable { + /// The session's working directories after the mutation. + public var directories: [String] + + public init( + directories: [String] + ) { + self.directories = directories + } +} + +public struct AddWorkspaceFolderResult: Codable, Sendable { + /// The session's working directories after the mutation. + public var directories: [String] + + public init( + directories: [String] + ) { + self.directories = directories + } +} + +public struct RemoveWorkspaceFolderResult: Codable, Sendable { + /// The session's working directories after the mutation. + public var directories: [String] + + public init( + directories: [String] + ) { + self.directories = directories + } +} + public struct ChatForkSource: Codable, Sendable { /// URI of the existing chat to fork from public var chat: String diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift index 0b39af3a7..09b0608ea 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift @@ -88,6 +88,14 @@ public enum AHPCommands { JsonRpcRequest(id: id, method: "disposeSession", params: params) } + public static func addWorkspaceFolder(id: Int, params: AddWorkspaceFolderParams) -> JsonRpcRequest { + JsonRpcRequest(id: id, method: "addWorkspaceFolder", params: params) + } + + public static func removeWorkspaceFolder(id: Int, params: RemoveWorkspaceFolderParams) -> JsonRpcRequest { + JsonRpcRequest(id: id, method: "removeWorkspaceFolder", params: params) + } + public static func listSessions(id: Int, params: ListSessionsParams) -> JsonRpcRequest { JsonRpcRequest(id: id, method: "listSessions", params: params) } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 6af2ddf19..dc9872c28 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -615,11 +615,19 @@ public struct AgentCapabilities: Codable, Sendable { /// session starts with. An empty object `{}` advertises multi-chat without /// forking; set {@link MultipleChatsCapability.fork} to also allow forking. public var multipleChats: MultipleChatsCapability? + /// The session's agent can be granted tool access to more than one working + /// directory, with all directories treated as equal peers (no primary). When + /// absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` + /// and MUST NOT set more than one entry in + /// {@link CreateSessionParams.workingDirectories}. + public var multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? public init( - multipleChats: MultipleChatsCapability? = nil + multipleChats: MultipleChatsCapability? = nil, + multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? = nil ) { self.multipleChats = multipleChats + self.multipleWorkspaceFolders = multipleWorkspaceFolders } } @@ -636,6 +644,14 @@ public struct MultipleChatsCapability: Codable, Sendable { } } +public struct MultipleWorkspaceFoldersCapability: Codable, Sendable { + + public init( + + ) { + } +} + public struct SessionModelInfo: Codable, Sendable { /// Model identifier public var id: String diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index eaca252b7..d6adc50c8 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -17,6 +17,10 @@ the tag matches the version pinned in [`VERSION`](VERSION). ## [Unreleased] +### Added + +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). + ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index d17e3cf93..835f00652 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -20,6 +20,10 @@ hotfix escape hatch. ## [Unreleased] +### Added + +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). + ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 7f0d67fa8..5d758baa6 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -73,7 +73,8 @@ SessionState { status: number // SessionStatus bitset activity?: string project?: ProjectInfo - workingDirectory?: URI + workingDirectory?: URI // deprecated: use workingDirectories + workingDirectories?: URI[] // equal-peer working directories annotations?: AnnotationsSummary lifecycle: 'creating' | 'ready' | 'creationFailed' @@ -107,7 +108,8 @@ SessionSummary { createdAt: string // ISO 8601, e.g. "2025-03-10T18:42:03.123Z" modifiedAt: string // ISO 8601 project?: ProjectInfo - workingDirectory?: URI + workingDirectory?: URI // deprecated: use workingDirectories + workingDirectories?: URI[] // equal-peer working directories annotations?: AnnotationsSummary changes?: ChangesSummary } @@ -551,6 +553,53 @@ createSession({ The forked session is an independent copy — subsequent changes to either session do not affect the other. The server broadcasts `root/sessionAdded` for the new session as usual. +## Multiroot Sessions + +A session can be granted tool access to more than one working directory when the +agent advertises the `multipleWorkspaceFolders` capability. All directories are +**equal peers** — there is no privileged "primary". + +### Creating a multiroot session + +Pass `workingDirectories` (plural) in `createSession`: + +```typescript +createSession({ + channel: 'ahp-session:/', + provider: 'copilot', + workingDirectories: [ + 'file:///workspace/frontend', + 'file:///workspace/backend', + ], +}); +``` + +A client MUST NOT pass more than one entry unless the agent advertises +`multipleWorkspaceFolders`. Servers without that capability treat only the +first entry as the session's working directory and ignore the rest. + +The singular `workingDirectory` field is deprecated in favour of +`workingDirectories` but is retained as a backwards-compatible shorthand; +servers MUST still honour it when `workingDirectories` is absent. + +Forked sessions ignore `workingDirectories` — they inherit the working +directories of the source session. + +### Managing directories after creation + +Two commands let clients mutate the directory set on a running session: + +| Command | Effect | +| --- | --- | +| `addWorkspaceFolder` | Grants tool access to the given directory. Adding a directory already in the set is a no-op. | +| `removeWorkspaceFolder` | Revokes tool access to the given directory. There is no atomic server-side "remove one" primitive; the server reconfigures to the reduced set. Removing a directory not in the set is a no-op. A server MAY return an error if it cannot relinquish a directory while the session is live. | + +Both commands return the **full directory set after the mutation** (the +`directories` field of `AddWorkspaceFolderResult` / `RemoveWorkspaceFolderResult`). + +Before issuing either command, a client MUST verify that the agent advertises +`multipleWorkspaceFolders`; servers MUST reject these commands otherwise. + ## Next Steps - [Actions](/guide/actions) — How state is mutated. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index bb4a1a28d..31a07408f 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -2568,6 +2568,10 @@ "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." + }, + "multipleWorkspaceFolders": { + "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -2581,6 +2585,11 @@ } } }, + "MultipleWorkspaceFoldersCapability": { + "type": "object", + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", + "properties": {} + }, "SessionModelInfo": { "type": "object", "properties": { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 2379858da..9ca94d977 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -955,9 +955,15 @@ "type": "string", "description": "Agent provider ID" }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories, all of which are equal peers —\nthere is no privileged \"primary\" directory.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder`\nto change the set after the session has started.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + }, "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Working directory for the session" + "$ref": "#/$defs/URI" }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -994,6 +1000,90 @@ "channel" ] }, + "AddWorkspaceFolderParams": { + "type": "object", + "description": "Grants the session's agent tool access to a working directory, adding it to\nthe session's set of {@link CreateSessionParams.workingDirectories | working\ndirectories}. Only valid when the agent advertises\n`multipleWorkspaceFolders`; servers MUST reject this command otherwise.\n\nAll directories in the set are equal peers — there is no privileged\n\"primary\". Adding a directory that is already in the set is a no-op that\nstill returns the current full set.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to grant tool access to." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "RemoveWorkspaceFolderParams": { + "type": "object", + "description": "Revokes the session's agent tool access to one of its working directories.\nThere is no server-side primitive to \"remove\" a single directory mid-session\n— the server instead reconfigures the agent with the reduced directory set\nand returns it, so this command is safe to model as idempotent: removing a\ndirectory that is not in the set is a no-op that still returns the current\nfull set.\n\nA server MAY refuse to remove a directory it cannot relinquish while the\nsession is live (for example one bound to the running agent process),\nreturning an error rather than a result. The protocol itself designates no\ndirectory as special.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to revoke tool access to." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "WorkspaceFolderResult": { + "type": "object", + "description": "Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder`\ncommands: the session's full set of working directories after the mutation.\nAll entries are equal peers; the order carries no meaning.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The session's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "AddWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The session's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "RemoveWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The session's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, "FetchTurnsParams": { "type": "object", "description": "Requests that the host load older historical turns into a chat state.\n\nThe command result does not carry turns. Instead, before responding, the host\nMUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat\nchannel's `turns` state, ahead of the already-loaded window, and update or\nclear `turnsNextCursor`.\n\nBefore applying any operation that references a turn outside the currently\nloaded window, the host MUST eagerly load enough older turns into state for\nthat operation to reduce against valid state.", @@ -1836,6 +1926,10 @@ "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." + }, + "multipleWorkspaceFolders": { + "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -1849,6 +1943,11 @@ } } }, + "MultipleWorkspaceFoldersCapability": { + "type": "object", + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", + "properties": {} + }, "SessionModelInfo": { "type": "object", "properties": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index e5d2b1ef0..b791efa79 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -627,6 +627,10 @@ "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." + }, + "multipleWorkspaceFolders": { + "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -640,6 +644,11 @@ } } }, + "MultipleWorkspaceFoldersCapability": { + "type": "object", + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", + "properties": {} + }, "SessionModelInfo": { "type": "object", "properties": { @@ -5607,9 +5616,15 @@ "type": "string", "description": "Agent provider ID" }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories, all of which are equal peers —\nthere is no privileged \"primary\" directory.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder`\nto change the set after the session has started.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + }, "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Working directory for the session" + "$ref": "#/$defs/URI" }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -5646,6 +5661,90 @@ "channel" ] }, + "AddWorkspaceFolderParams": { + "type": "object", + "description": "Grants the session's agent tool access to a working directory, adding it to\nthe session's set of {@link CreateSessionParams.workingDirectories | working\ndirectories}. Only valid when the agent advertises\n`multipleWorkspaceFolders`; servers MUST reject this command otherwise.\n\nAll directories in the set are equal peers — there is no privileged\n\"primary\". Adding a directory that is already in the set is a no-op that\nstill returns the current full set.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to grant tool access to." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "RemoveWorkspaceFolderParams": { + "type": "object", + "description": "Revokes the session's agent tool access to one of its working directories.\nThere is no server-side primitive to \"remove\" a single directory mid-session\n— the server instead reconfigures the agent with the reduced directory set\nand returns it, so this command is safe to model as idempotent: removing a\ndirectory that is not in the set is a no-op that still returns the current\nfull set.\n\nA server MAY refuse to remove a directory it cannot relinquish while the\nsession is live (for example one bound to the running agent process),\nreturning an error rather than a result. The protocol itself designates no\ndirectory as special.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to revoke tool access to." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "WorkspaceFolderResult": { + "type": "object", + "description": "Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder`\ncommands: the session's full set of working directories after the mutation.\nAll entries are equal peers; the order carries no meaning.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The session's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "AddWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The session's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "RemoveWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The session's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, "FetchTurnsParams": { "type": "object", "description": "Requests that the host load older historical turns into a chat state.\n\nThe command result does not carry turns. Instead, before responding, the host\nMUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat\nchannel's `turns` state, ahead of the already-loaded window, and update or\nclear `turnsNextCursor`.\n\nBefore applying any operation that references a turn outside the currently\nloaded window, the host MUST eagerly load enough older turns into state for\nthat operation to reduce against valid state.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 85e21832c..65acea3be 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -787,6 +787,10 @@ "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." + }, + "multipleWorkspaceFolders": { + "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -800,6 +804,11 @@ } } }, + "MultipleWorkspaceFoldersCapability": { + "type": "object", + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", + "properties": {} + }, "SessionModelInfo": { "type": "object", "properties": { diff --git a/schema/state.schema.json b/schema/state.schema.json index 5017391e0..5d74157a1 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -538,6 +538,10 @@ "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." + }, + "multipleWorkspaceFolders": { + "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -551,6 +555,11 @@ } } }, + "MultipleWorkspaceFoldersCapability": { + "type": "object", + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", + "properties": {} + }, "SessionModelInfo": { "type": "object", "properties": { diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 191766998..4e1dc3fc9 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -659,6 +659,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'AgentInfo' }, { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, + { name: 'MultipleWorkspaceFoldersCapability' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, @@ -1435,6 +1436,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'SubscribeParams' }, { name: 'SubscribeView' }, { name: 'SubscriptionDeliveryOptions' }, { name: 'SubscribeResult' }, { name: 'SessionForkSource' }, { name: 'CreateSessionParams' }, { name: 'DisposeSessionParams' }, + { name: 'AddWorkspaceFolderParams' }, { name: 'RemoveWorkspaceFolderParams' }, + { name: 'WorkspaceFolderResult' }, { name: 'AddWorkspaceFolderResult' }, { name: 'RemoveWorkspaceFolderResult' }, { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, { name: 'ResourceReadParams' }, { name: 'ResourceReadResult' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 9ab6b405a..8a8e9714f 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -802,6 +802,7 @@ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'AgentCapabilities', 'MultipleChatsCapability', + 'MultipleWorkspaceFoldersCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', @@ -1409,6 +1410,8 @@ const COMMAND_STRUCTS = [ 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', + 'AddWorkspaceFolderParams', 'RemoveWorkspaceFolderParams', + 'WorkspaceFolderResult', 'AddWorkspaceFolderResult', 'RemoveWorkspaceFolderResult', 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', @@ -1769,6 +1772,12 @@ object AhpCommands { fun disposeSession(id: Long, params: DisposeSessionParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "disposeSession", params = params) + fun addWorkspaceFolder(id: Long, params: AddWorkspaceFolderParams): JsonRpcRequest = + JsonRpcRequest(id = id, method = "addWorkspaceFolder", params = params) + + fun removeWorkspaceFolder(id: Long, params: RemoveWorkspaceFolderParams): JsonRpcRequest = + JsonRpcRequest(id = id, method = "removeWorkspaceFolder", params = params) + fun listSessions(id: Long, params: ListSessionsParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "listSessions", params = params) diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index d29e56f12..bc149facd 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -690,6 +690,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'AgentInfo' }, { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, + { name: 'MultipleWorkspaceFoldersCapability' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, @@ -1384,6 +1385,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'SubscribeParams' }, { name: 'SubscribeView' }, { name: 'SubscriptionDeliveryOptions' }, { name: 'SubscribeResult' }, { name: 'SessionForkSource' }, { name: 'CreateSessionParams' }, { name: 'DisposeSessionParams' }, + { name: 'AddWorkspaceFolderParams' }, { name: 'RemoveWorkspaceFolderParams' }, + { name: 'WorkspaceFolderResult' }, { name: 'AddWorkspaceFolderResult' }, { name: 'RemoveWorkspaceFolderResult' }, { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index d168d1e88..b36f759b3 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -552,6 +552,7 @@ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'AgentCapabilities', 'MultipleChatsCapability', + 'MultipleWorkspaceFoldersCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', @@ -1327,6 +1328,8 @@ const COMMAND_STRUCTS = [ 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', + 'AddWorkspaceFolderParams', 'RemoveWorkspaceFolderParams', + 'WorkspaceFolderResult', 'AddWorkspaceFolderResult', 'RemoveWorkspaceFolderResult', 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', @@ -1686,6 +1689,14 @@ public enum AHPCommands { JsonRpcRequest(id: id, method: "disposeSession", params: params) } + public static func addWorkspaceFolder(id: Int, params: AddWorkspaceFolderParams) -> JsonRpcRequest { + JsonRpcRequest(id: id, method: "addWorkspaceFolder", params: params) + } + + public static func removeWorkspaceFolder(id: Int, params: RemoveWorkspaceFolderParams) -> JsonRpcRequest { + JsonRpcRequest(id: id, method: "removeWorkspaceFolder", params: params) + } + public static func listSessions(id: Int, params: ListSessionsParams) -> JsonRpcRequest { JsonRpcRequest(id: id, method: "listSessions", params: params) } diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index 08a21371a..fb1841f5b 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -110,6 +110,14 @@ export interface AgentCapabilities { * forking; set {@link MultipleChatsCapability.fork} to also allow forking. */ multipleChats?: MultipleChatsCapability; + /** + * The session's agent can be granted tool access to more than one working + * directory, with all directories treated as equal peers (no primary). When + * absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` + * and MUST NOT set more than one entry in + * {@link CreateSessionParams.workingDirectories}. + */ + multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability; } /** @@ -126,6 +134,15 @@ export interface MultipleChatsCapability { fork?: boolean; } +/** + * Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. + * Currently carries no sub-options; presence of an empty object `{}` is the + * entire signal. + * + * @category Root State + */ +export interface MultipleWorkspaceFoldersCapability {} + /** * @category Root State */ diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index d6b11eb14..332960f09 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -66,7 +66,27 @@ export interface CreateSessionParams extends BaseParams { channel: URI; /** Agent provider ID */ provider?: string; - /** Working directory for the session */ + /** + * The working directories the session's agent is granted tool access to. + * A session may span multiple directories, all of which are equal peers — + * there is no privileged "primary" directory. + * + * A client MUST NOT supply more than one entry unless the agent advertises + * {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + * capability treats only the first entry as the session's working directory + * and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` + * to change the set after the session has started. + * + * Ignored for forked sessions — a fork inherits its working directories + * from the source session identified by `fork`. + */ + workingDirectories?: URI[]; + /** + * @deprecated Use {@link workingDirectories} instead. Retained as a + * single-directory shorthand for backwards compatibility: when set, it is + * equivalent to `workingDirectories: [workingDirectory]`. If both are + * provided, `workingDirectories` takes precedence and this field is ignored. + */ workingDirectory?: URI; /** * Fork from an existing session. The new session is populated with content @@ -117,6 +137,75 @@ export interface CreateSessionParams extends BaseParams { */ export interface DisposeSessionParams extends BaseParams {} +// ─── addWorkspaceFolder ────────────────────────────────────────────────────── + +/** + * Grants the session's agent tool access to a working directory, adding it to + * the session's set of {@link CreateSessionParams.workingDirectories | working + * directories}. Only valid when the agent advertises + * `multipleWorkspaceFolders`; servers MUST reject this command otherwise. + * + * All directories in the set are equal peers — there is no privileged + * "primary". Adding a directory that is already in the set is a no-op that + * still returns the current full set. + * + * @category Commands + * @method addWorkspaceFolder + * @direction Client → Server + * @messageType Request + * @version 0.6.0 + */ +export interface AddWorkspaceFolderParams extends BaseParams { + /** Directory to grant tool access to. */ + folder: URI; +} + +// ─── removeWorkspaceFolder ─────────────────────────────────────────────────── + +/** + * Revokes the session's agent tool access to one of its working directories. + * There is no server-side primitive to "remove" a single directory mid-session + * — the server instead reconfigures the agent with the reduced directory set + * and returns it, so this command is safe to model as idempotent: removing a + * directory that is not in the set is a no-op that still returns the current + * full set. + * + * A server MAY refuse to remove a directory it cannot relinquish while the + * session is live (for example one bound to the running agent process), + * returning an error rather than a result. The protocol itself designates no + * directory as special. + * + * @category Commands + * @method removeWorkspaceFolder + * @direction Client → Server + * @messageType Request + * @version 0.6.0 + */ +export interface RemoveWorkspaceFolderParams extends BaseParams { + /** Directory to revoke tool access to. */ + folder: URI; +} + +/** + * Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder` + * commands: the session's full set of working directories after the mutation. + * All entries are equal peers; the order carries no meaning. + */ +export interface WorkspaceFolderResult { + /** The session's working directories after the mutation. */ + directories: URI[]; +} + +/** + * Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}. + */ +export interface AddWorkspaceFolderResult extends WorkspaceFolderResult {} + +/** + * Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}. + */ +export interface RemoveWorkspaceFolderResult extends WorkspaceFolderResult {} + // ─── fetchTurns ────────────────────────────────────────────────────────────── /** diff --git a/types/common/messages.ts b/types/common/messages.ts index e248b9e26..8daed9de2 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -49,6 +49,10 @@ import type { import type { CreateSessionParams, DisposeSessionParams, + AddWorkspaceFolderParams, + AddWorkspaceFolderResult, + RemoveWorkspaceFolderParams, + RemoveWorkspaceFolderResult, FetchTurnsParams, FetchTurnsResult, CompletionsParams, @@ -153,6 +157,8 @@ export interface CommandMap { 'subscribe': { params: SubscribeParams; result: SubscribeResult }; 'createSession': { params: CreateSessionParams; result: null }; 'disposeSession': { params: DisposeSessionParams; result: null }; + 'addWorkspaceFolder': { params: AddWorkspaceFolderParams; result: AddWorkspaceFolderResult }; + 'removeWorkspaceFolder': { params: RemoveWorkspaceFolderParams; result: RemoveWorkspaceFolderResult }; 'createChat': { params: CreateChatParams; result: null }; 'disposeChat': { params: DisposeChatParams; result: null }; 'createTerminal': { params: CreateTerminalParams; result: null }; diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index 8da4dddf5..acd5807db 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -56,6 +56,8 @@ type _ExpectedCommands = | 'subscribe' | 'createSession' | 'disposeSession' + | 'addWorkspaceFolder' + | 'removeWorkspaceFolder' | 'createChat' | 'disposeChat' | 'createTerminal' From d53cdac57f9a5aa71772c0602e9459b92e3520d1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 Jul 2026 16:34:09 +0200 Subject: [PATCH 02/15] spec: per-chat working-directory subsets (AHP 0.6.0) Extends the multiroot session design with per-chat directory subsets: - Add `SessionMetadata.workingDirectories` (on `SessionState` and `SessionSummary`), the mirror of the `CreateSessionParams.workingDirectories` set as maintained by `addWorkspaceFolder`/`removeWorkspaceFolder`. Deprecate the singular `workingDirectory` on `SessionMetadata`; hosts SHOULD keep populating it (first entry) for backwards compatibility. - Add `ChatState.workingDirectories` and `ChatSummary.workingDirectories`: the subset of the session's `workingDirectories` that this chat's agent has tool access to. When absent the chat inherits the full session set. Every entry must be present in the session's set. Deprecate the singular `workingDirectory` on both. - Add `CreateChatParams.workingDirectories`: initial subset at create time. Ignored for forked chats (they inherit the source chat's set). Gated on `multipleWorkspaceFolders` capability. - Add `addChatWorkspaceFolder` and `removeChatWorkspaceFolder` commands (symmetrical to the session-level pair) for mutating the chat's subset on a running chat. The add command enforces the subset invariant (server rejects any directory not in the session's set). The remove command is idempotent. Both return `AddChatWorkspaceFolderResult` / `RemoveChatWorkspaceFolderResult` carrying the chat's full subset after the mutation. - Register both commands in `CommandMap`, `_ExpectedCommands`, all four generator scripts, and all CHANGELOGs. - Update `docs/guide/state-model.md` with a "Per-chat working-directory subsets" subsection and updated `ChatState` code snippet. All 316 tests pass; 100% branch coverage maintained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 41 +++-- clients/go/CHANGELOG.md | 2 +- clients/go/ahptypes/commands.generated.go | 57 +++++++ .../go/ahptypes/notifications.generated.go | 9 ++ clients/go/ahptypes/state.generated.go | 49 ++++-- clients/kotlin/CHANGELOG.md | 2 +- .../generated/Actions.generated.kt | 11 +- .../generated/Commands.generated.kt | 61 +++++++- .../generated/Notifications.generated.kt | 11 ++ .../generated/State.generated.kt | 53 +++++-- clients/rust/CHANGELOG.md | 2 +- clients/rust/crates/ahp-types/src/actions.rs | 8 +- clients/rust/crates/ahp-types/src/commands.rs | 68 +++++++++ .../crates/ahp-types/src/notifications.rs | 10 ++ clients/rust/crates/ahp-types/src/state.rs | 53 +++++-- .../Generated/Actions.generated.swift | 11 +- .../Generated/Commands.generated.swift | 76 +++++++++- .../Generated/Notifications.generated.swift | 12 ++ .../Generated/State.generated.swift | 56 +++++-- clients/swift/CHANGELOG.md | 2 +- clients/typescript/CHANGELOG.md | 2 +- docs/guide/state-model.md | 46 +++++- schema/actions.schema.json | 50 ++++++- schema/commands.schema.json | 141 +++++++++++++++++- schema/errors.schema.json | 141 +++++++++++++++++- schema/notifications.schema.json | 48 +++++- schema/state.schema.json | 41 ++++- scripts/generate-go.ts | 2 + scripts/generate-kotlin.ts | 2 + scripts/generate-rust.ts | 2 + scripts/generate-swift.ts | 2 + types/channels-chat/commands.ts | 78 +++++++++- types/channels-chat/state.ts | 34 ++++- types/channels-session/state.ts | 22 ++- types/common/messages.ts | 6 + types/version/message-checks.ts | 2 + 36 files changed, 1103 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45723c478..fbbea1e7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,28 +31,49 @@ Spec version: `0.6.0` - `multipleWorkspaceFolders` capability on `AgentCapabilities`, gating first-class multiroot session support. When absent, clients MUST NOT call - `addWorkspaceFolder` / `removeWorkspaceFolder` and MUST NOT set more than one - entry in `CreateSessionParams.workingDirectories`. -- `CreateSessionParams.workingDirectories` — the set of working directories the - session's agent is granted tool access to. All entries are equal peers with no - privileged "primary". Ignored for forked sessions (a fork inherits its - working directories from the source session). + `addWorkspaceFolder` / `removeWorkspaceFolder` / `addChatWorkspaceFolder` / + `removeChatWorkspaceFolder` and MUST NOT set more than one entry in + `CreateSessionParams.workingDirectories` or `CreateChatParams.workingDirectories`. +- `SessionMetadata.workingDirectories` (on `SessionState` and `SessionSummary`) — + the full set of working directories the session's agent has tool access to, + maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All entries are + equal peers with no privileged "primary". +- `CreateSessionParams.workingDirectories` — initial working-directory set for + the session. Ignored for forked sessions. - `addWorkspaceFolder` command — grants a running session's agent tool access to an additional working directory. Returns `AddWorkspaceFolderResult` carrying the full directory set after the mutation. - `removeWorkspaceFolder` command — revokes tool access to one working directory by reconfiguring the agent to the reduced set. There is no atomic server-side - "remove one" primitive; the command is idempotent (removing a directory not in - the set is a no-op that still returns the current set). Returns + "remove one" primitive; the command is idempotent. Returns `RemoveWorkspaceFolderResult` carrying the full directory set after the mutation. +- `ChatState.workingDirectories` / `ChatSummary.workingDirectories` — the + subset of the session's `workingDirectories` that a specific chat's agent has + tool access to. When absent the chat inherits the full session set. +- `CreateChatParams.workingDirectories` — initial working-directory subset for a + new chat. Every entry must already be present in the session's + `workingDirectories`. Ignored for forked chats (inherit source chat's set). +- `addChatWorkspaceFolder` command — adds a directory to a chat's + working-directory subset (must already be in the session's set). Returns + `AddChatWorkspaceFolderResult` with the chat's full subset after the mutation. +- `removeChatWorkspaceFolder` command — removes a directory from a chat's + working-directory subset (idempotent). Returns `RemoveChatWorkspaceFolderResult` + with the chat's full subset after the mutation. ### Deprecated - `CreateSessionParams.workingDirectory` (singular) in favour of `workingDirectories`. Retained as a single-directory shorthand for backwards - compatibility; servers MUST still honour it when `workingDirectories` is - absent. + compatibility; servers MUST still honour it when `workingDirectories` is absent. +- `SessionMetadata.workingDirectory` (singular, on `SessionState` and + `SessionSummary`) in favour of `workingDirectories`. Hosts SHOULD keep + populating the singular field (set to the first entry of `workingDirectories`) + so clients that predate 0.6.0 continue to see a usable value. +- `ChatState.workingDirectory` / `ChatSummary.workingDirectory` (singular) in + favour of `workingDirectories`. Hosts SHOULD keep populating the singular + field (first entry of the chat's `workingDirectories`) for backwards + compatibility. ## [0.5.2] — 2026-07-09 diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index 3c702b5d3..b11460f6c 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -16,7 +16,7 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). ## [0.5.2] — 2026-07-09 diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 5b767cd75..4a86ad529 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -424,6 +424,15 @@ type CreateChatParams struct { InitialMessage *Message `json:"initialMessage,omitempty"` // Optional source chat and turn to fork from. Source *ChatForkSource `json:"source,omitempty"` + // Initial working-directory subset for this chat. Every entry MUST be + // present in the owning session's `workingDirectories`; the server MUST + // reject any entry that is not. When absent, the chat inherits the full + // session set. Forked chats (`source`) inherit the source chat's + // `workingDirectories`; this field is ignored for forked chats. + // + // A client MUST NOT supply this field unless the agent advertises + // {@link AgentCapabilities.multipleWorkspaceFolders}. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` } // Disposes a chat and cleans up server-side resources. @@ -432,6 +441,54 @@ type DisposeChatParams struct { Channel URI `json:"channel"` } +// Grants this chat's agent tool access to a working directory, adding it to +// the chat's {@link ChatState.workingDirectories | `workingDirectories`} subset. +// The directory MUST already be present in the owning session's +// `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. +// +// Only valid when the agent advertises `multipleWorkspaceFolders`. Adding a +// directory already in the chat's set is a no-op that still returns the current +// full set. +type AddChatWorkspaceFolderParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Directory to grant tool access to. Must be in the session's `workingDirectories`. + Folder URI `json:"folder"` +} + +// Revokes this chat's agent tool access to one of its working directories. +// Analogous to the session-level `removeWorkspaceFolder`: the server +// reconfigures the chat to the reduced subset and returns it. Removing a +// directory not in the chat's set is a no-op that still returns the current +// full set. +// +// Only valid when the agent advertises `multipleWorkspaceFolders`. +type RemoveChatWorkspaceFolderParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Directory to revoke tool access to. + Folder URI `json:"folder"` +} + +// Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`: +// the chat's full working-directory subset after the mutation. +type ChatWorkspaceFolderResult struct { + // The chat's working directories after the mutation. + Directories []URI `json:"directories"` +} + +// Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. +type AddChatWorkspaceFolderResult struct { + // The chat's working directories after the mutation. + Directories []URI `json:"directories"` +} + +// Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. +type RemoveChatWorkspaceFolderResult struct { + // The chat's working directories after the mutation. + Directories []URI `json:"directories"` +} + // Returns a list of session summaries. Used to populate session lists and sidebars. // // The session list is **not** part of the state tree because it can be arbitrarily diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 6c214d5fc..8268b992a 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -222,6 +222,15 @@ type PartialSessionSummary struct { // `workingDirectory`}; this field acts as the fallback for any chat that // does not. WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // The full set of working directories the session's agent has tool access + // to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + // entries are equal peers — there is no privileged "primary". Individual + // chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + // + // When absent, fall back to {@link workingDirectory} (if set) as a + // single-entry set. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 1324c33d1..3d8a95271 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -710,6 +710,15 @@ type SessionState struct { // `workingDirectory`}; this field acts as the fallback for any chat that // does not. WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // The full set of working directories the session's agent has tool access + // to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + // entries are equal peers — there is no privileged "primary". Individual + // chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + // + // When absent, fall back to {@link workingDirectory} (if set) as a + // single-entry set. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -914,9 +923,9 @@ type SessionToolClientExecutionRequest struct { // chat currently driving the promoted status bits when a non-default chat // wins (e.g. the chat that raised `InputNeeded`). // - `modifiedAt`: the max of all chats' `modifiedAt`. -// - `workingDirectory`: the session-level **default**. Individual chats MAY -// override via {@link ChatSummary.workingDirectory}; aggregating these up -// is meaningless and SHOULD NOT be attempted. +// - `workingDirectory` / `workingDirectories`: the session-level set. Individual +// chats MAY restrict to a subset via {@link ChatSummary.workingDirectories}; +// aggregating these up is meaningless and SHOULD NOT be attempted. // - `changes`: optional roll-up across all chats. Producers MAY sum the // per-chat changeset stats or report the most expensive chat's stats — // whichever is cheaper for the host to compute. @@ -940,6 +949,15 @@ type SessionSummary struct { // `workingDirectory`}; this field acts as the fallback for any chat that // does not. WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // The full set of working directories the session's agent has tool access + // to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + // entries are equal peers — there is no privileged "primary". Individual + // chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + // + // When absent, fall back to {@link workingDirectory} (if set) as a + // single-entry set. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -1007,13 +1025,19 @@ type ChatState struct { // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` // Optional per-chat working directory. - // - // If absent, the chat inherits - // {@link SessionState.workingDirectory | the session's working directory}. - // Hosts MAY override this for individual chats — for example, to give a - // subordinate chat its own git worktree so multiple chats in a session can - // make independent edits that the orchestrator later merges back. WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // The subset of the session's + // {@link SessionState.workingDirectories | `workingDirectories`} that this + // chat's agent has tool access to. Every entry MUST be present in the owning + // session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` + // calls that violate this constraint. + // + // When absent, the chat inherits the full session set. When present but empty + // (not recommended), the chat has no working-directory tool access at all. + // + // Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the + // set on a running chat. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Completed turns Turns []Turn `json:"turns"` // Cursor for loading older completed turns into this chat state. @@ -1070,11 +1094,10 @@ type ChatSummary struct { // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` // Optional per-chat working directory. - // - // If absent, the chat inherits - // {@link SessionSummary.workingDirectory | the session's working directory}. - // See {@link ChatState.workingDirectory} for usage notes. WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // The subset of the session's working directories this chat uses. + // See {@link ChatState.workingDirectories} for the full semantics. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` } // A message queued for future delivery to the agent. diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 321786c52..f7379149f 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -17,7 +17,7 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). ## [0.5.2] — 2026-07-09 diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index 4a9517c45..89b7e3402 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -1406,12 +1406,13 @@ data class PartialChatSummary( val interactivity: ChatInteractivity? = null, /** * Optional per-chat working directory. - * - * If absent, the chat inherits - * {@link SessionSummary.workingDirectory | the session's working directory}. - * See {@link ChatState.workingDirectory} for usage notes. */ - val workingDirectory: String? = null + val workingDirectory: String? = null, + /** + * The subset of the session's working directories this chat uses. + * See {@link ChatState.workingDirectories} for the full semantics. + */ + val workingDirectories: List? = null ) // ─── StateAction Union ────────────────────────────────────────────────────── diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index d6676a344..da787df7c 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -503,7 +503,18 @@ data class CreateChatParams( /** * Optional source chat and turn to fork from. */ - val source: ChatForkSource? = null + val source: ChatForkSource? = null, + /** + * Initial working-directory subset for this chat. Every entry MUST be + * present in the owning session's `workingDirectories`; the server MUST + * reject any entry that is not. When absent, the chat inherits the full + * session set. Forked chats (`source`) inherit the source chat's + * `workingDirectories`; this field is ignored for forked chats. + * + * A client MUST NOT supply this field unless the agent advertises + * {@link AgentCapabilities.multipleWorkspaceFolders}. + */ + val workingDirectories: List? = null ) @Serializable @@ -514,6 +525,54 @@ data class DisposeChatParams( val channel: String ) +@Serializable +data class AddChatWorkspaceFolderParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Directory to grant tool access to. Must be in the session's `workingDirectories`. + */ + val folder: String +) + +@Serializable +data class RemoveChatWorkspaceFolderParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Directory to revoke tool access to. + */ + val folder: String +) + +@Serializable +data class ChatWorkspaceFolderResult( + /** + * The chat's working directories after the mutation. + */ + val directories: List +) + +@Serializable +data class AddChatWorkspaceFolderResult( + /** + * The chat's working directories after the mutation. + */ + val directories: List +) + +@Serializable +data class RemoveChatWorkspaceFolderResult( + /** + * The chat's working directories after the mutation. + */ + val directories: List +) + @Serializable data class ListSessionsParams( /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 202766a37..d2a1779b3 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -202,6 +202,17 @@ data class PartialSessionSummary( * does not. */ val workingDirectory: String? = null, + /** + * The full set of working directories the session's agent has tool access + * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + * entries are equal peers — there is no privileged "primary". Individual + * chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + * + * When absent, fall back to {@link workingDirectory} (if set) as a + * single-entry set. + */ + val workingDirectories: List? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 603482fcc..2b7566055 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -1132,14 +1132,22 @@ data class ChatState( val interactivity: ChatInteractivity? = null, /** * Optional per-chat working directory. - * - * If absent, the chat inherits - * {@link SessionState.workingDirectory | the session's working directory}. - * Hosts MAY override this for individual chats — for example, to give a - * subordinate chat its own git worktree so multiple chats in a session can - * make independent edits that the orchestrator later merges back. */ val workingDirectory: String? = null, + /** + * The subset of the session's + * {@link SessionState.workingDirectories | `workingDirectories`} that this + * chat's agent has tool access to. Every entry MUST be present in the owning + * session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` + * calls that violate this constraint. + * + * When absent, the chat inherits the full session set. When present but empty + * (not recommended), the chat has no working-directory tool access at all. + * + * Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the + * set on a running chat. + */ + val workingDirectories: List? = null, /** * Completed turns */ @@ -1226,12 +1234,13 @@ data class ChatSummary( val interactivity: ChatInteractivity? = null, /** * Optional per-chat working directory. - * - * If absent, the chat inherits - * {@link SessionSummary.workingDirectory | the session's working directory}. - * See {@link ChatState.workingDirectory} for usage notes. */ - val workingDirectory: String? = null + val workingDirectory: String? = null, + /** + * The subset of the session's working directories this chat uses. + * See {@link ChatState.workingDirectories} for the full semantics. + */ + val workingDirectories: List? = null ) @Serializable @@ -1263,6 +1272,17 @@ data class SessionState( * does not. */ val workingDirectory: String? = null, + /** + * The full set of working directories the session's agent has tool access + * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + * entries are equal peers — there is no privileged "primary". Individual + * chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + * + * When absent, fall back to {@link workingDirectory} (if set) as a + * single-entry set. + */ + val workingDirectories: List? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1505,6 +1525,17 @@ data class SessionSummary( * does not. */ val workingDirectory: String? = null, + /** + * The full set of working directories the session's agent has tool access + * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + * entries are equal peers — there is no privileged "primary". Individual + * chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + * + * When absent, fall back to {@link workingDirectory} (if set) as a + * single-entry set. + */ + val workingDirectories: List? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 592ae5ad5..b48afada8 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -17,7 +17,7 @@ matching `## [X.Y.Z]` heading is missing from this file. ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). ## [0.5.2] — 2026-07-09 diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index c76109d8b..ba03df425 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1611,12 +1611,12 @@ pub struct PartialChatSummary { #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, /// Optional per-chat working directory. - /// - /// If absent, the chat inherits - /// {@link SessionSummary.workingDirectory | the session's working directory}. - /// See {@link ChatState.workingDirectory} for usage notes. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// The subset of the session's working directories this chat uses. + /// See {@link ChatState.workingDirectories} for the full semantics. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, } // ─── StateAction Union ─────────────────────────────────────────────── diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 9e40355b9..b3d0f4524 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -534,6 +534,16 @@ pub struct CreateChatParams { /// Optional source chat and turn to fork from. #[serde(default, skip_serializing_if = "Option::is_none")] pub source: Option, + /// Initial working-directory subset for this chat. Every entry MUST be + /// present in the owning session's `workingDirectories`; the server MUST + /// reject any entry that is not. When absent, the chat inherits the full + /// session set. Forked chats (`source`) inherit the source chat's + /// `workingDirectories`; this field is ignored for forked chats. + /// + /// A client MUST NOT supply this field unless the agent advertises + /// {@link AgentCapabilities.multipleWorkspaceFolders}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, } /// Disposes a chat and cleans up server-side resources. @@ -544,6 +554,64 @@ pub struct DisposeChatParams { pub channel: Uri, } +/// Grants this chat's agent tool access to a working directory, adding it to +/// the chat's {@link ChatState.workingDirectories | `workingDirectories`} subset. +/// The directory MUST already be present in the owning session's +/// `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. +/// +/// Only valid when the agent advertises `multipleWorkspaceFolders`. Adding a +/// directory already in the chat's set is a no-op that still returns the current +/// full set. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddChatWorkspaceFolderParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Directory to grant tool access to. Must be in the session's `workingDirectories`. + pub folder: Uri, +} + +/// Revokes this chat's agent tool access to one of its working directories. +/// Analogous to the session-level `removeWorkspaceFolder`: the server +/// reconfigures the chat to the reduced subset and returns it. Removing a +/// directory not in the chat's set is a no-op that still returns the current +/// full set. +/// +/// Only valid when the agent advertises `multipleWorkspaceFolders`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveChatWorkspaceFolderParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Directory to revoke tool access to. + pub folder: Uri, +} + +/// Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`: +/// the chat's full working-directory subset after the mutation. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatWorkspaceFolderResult { + /// The chat's working directories after the mutation. + pub directories: Vec, +} + +/// Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddChatWorkspaceFolderResult { + /// The chat's working directories after the mutation. + pub directories: Vec, +} + +/// Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveChatWorkspaceFolderResult { + /// The chat's working directories after the mutation. + pub directories: Vec, +} + /// Returns a list of session summaries. Used to populate session lists and sidebars. /// /// The session list is **not** part of the state tree because it can be arbitrarily diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 5621f1a52..b78bdd69d 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -254,6 +254,16 @@ pub struct PartialSessionSummary { /// does not. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// The full set of working directories the session's agent has tool access + /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + /// entries are equal peers — there is no privileged "primary". Individual + /// chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + /// + /// When absent, fall back to {@link workingDirectory} (if set) as a + /// single-entry set. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index e9cd52746..6bf0d7c8f 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -987,14 +987,21 @@ pub struct ChatState { #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, /// Optional per-chat working directory. - /// - /// If absent, the chat inherits - /// {@link SessionState.workingDirectory | the session's working directory}. - /// Hosts MAY override this for individual chats — for example, to give a - /// subordinate chat its own git worktree so multiple chats in a session can - /// make independent edits that the orchestrator later merges back. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// The subset of the session's + /// {@link SessionState.workingDirectories | `workingDirectories`} that this + /// chat's agent has tool access to. Every entry MUST be present in the owning + /// session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` + /// calls that violate this constraint. + /// + /// When absent, the chat inherits the full session set. When present but empty + /// (not recommended), the chat has no working-directory tool access at all. + /// + /// Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the + /// set on a running chat. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, /// Completed turns pub turns: Vec, /// Cursor for loading older completed turns into this chat state. @@ -1063,12 +1070,12 @@ pub struct ChatSummary { #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, /// Optional per-chat working directory. - /// - /// If absent, the chat inherits - /// {@link SessionSummary.workingDirectory | the session's working directory}. - /// See {@link ChatState.workingDirectory} for usage notes. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// The subset of the session's working directories this chat uses. + /// See {@link ChatState.workingDirectories} for the full semantics. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, } /// Full state for a single session, loaded when a client subscribes to the session's URI. @@ -1099,6 +1106,16 @@ pub struct SessionState { /// does not. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// The full set of working directories the session's agent has tool access + /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + /// entries are equal peers — there is no privileged "primary". Individual + /// chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + /// + /// When absent, fall back to {@link workingDirectory} (if set) as a + /// single-entry set. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1319,9 +1336,9 @@ pub struct SessionToolClientExecutionRequest { /// chat currently driving the promoted status bits when a non-default chat /// wins (e.g. the chat that raised `InputNeeded`). /// - `modifiedAt`: the max of all chats' `modifiedAt`. -/// - `workingDirectory`: the session-level **default**. Individual chats MAY -/// override via {@link ChatSummary.workingDirectory}; aggregating these up -/// is meaningless and SHOULD NOT be attempted. +/// - `workingDirectory` / `workingDirectories`: the session-level set. Individual +/// chats MAY restrict to a subset via {@link ChatSummary.workingDirectories}; +/// aggregating these up is meaningless and SHOULD NOT be attempted. /// - `changes`: optional roll-up across all chats. Producers MAY sum the /// per-chat changeset stats or report the most expensive chat's stats — /// whichever is cheaper for the host to compute. @@ -1350,6 +1367,16 @@ pub struct SessionSummary { /// does not. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// The full set of working directories the session's agent has tool access + /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + /// entries are equal peers — there is no privileged "primary". Individual + /// chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + /// + /// When absent, fall back to {@link workingDirectory} (if set) as a + /// single-entry set. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 76473df36..a43c6e454 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1837,11 +1837,10 @@ public struct PartialChatSummary: Codable, Sendable { /// compatibility. public var interactivity: ChatInteractivity? /// Optional per-chat working directory. - /// - /// If absent, the chat inherits - /// {@link SessionSummary.workingDirectory | the session's working directory}. - /// See {@link ChatState.workingDirectory} for usage notes. public var workingDirectory: String? + /// The subset of the session's working directories this chat uses. + /// See {@link ChatState.workingDirectories} for the full semantics. + public var workingDirectories: [String]? public init( resource: String? = nil, @@ -1851,7 +1850,8 @@ public struct PartialChatSummary: Codable, Sendable { modifiedAt: String? = nil, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, - workingDirectory: String? = nil + workingDirectory: String? = nil, + workingDirectories: [String]? = nil ) { self.resource = resource self.title = title @@ -1861,6 +1861,7 @@ public struct PartialChatSummary: Codable, Sendable { self.origin = origin self.interactivity = interactivity self.workingDirectory = workingDirectory + self.workingDirectories = workingDirectories } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 20f4d8c8d..acafb5b97 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -512,17 +512,28 @@ public struct CreateChatParams: Codable, Sendable { public var initialMessage: Message? /// Optional source chat and turn to fork from. public var source: ChatForkSource? + /// Initial working-directory subset for this chat. Every entry MUST be + /// present in the owning session's `workingDirectories`; the server MUST + /// reject any entry that is not. When absent, the chat inherits the full + /// session set. Forked chats (`source`) inherit the source chat's + /// `workingDirectories`; this field is ignored for forked chats. + /// + /// A client MUST NOT supply this field unless the agent advertises + /// {@link AgentCapabilities.multipleWorkspaceFolders}. + public var workingDirectories: [String]? public init( channel: String, chat: String, initialMessage: Message? = nil, - source: ChatForkSource? = nil + source: ChatForkSource? = nil, + workingDirectories: [String]? = nil ) { self.channel = channel self.chat = chat self.initialMessage = initialMessage self.source = source + self.workingDirectories = workingDirectories } } @@ -537,6 +548,69 @@ public struct DisposeChatParams: Codable, Sendable { } } +public struct AddChatWorkspaceFolderParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Directory to grant tool access to. Must be in the session's `workingDirectories`. + public var folder: String + + public init( + channel: String, + folder: String + ) { + self.channel = channel + self.folder = folder + } +} + +public struct RemoveChatWorkspaceFolderParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Directory to revoke tool access to. + public var folder: String + + public init( + channel: String, + folder: String + ) { + self.channel = channel + self.folder = folder + } +} + +public struct ChatWorkspaceFolderResult: Codable, Sendable { + /// The chat's working directories after the mutation. + public var directories: [String] + + public init( + directories: [String] + ) { + self.directories = directories + } +} + +public struct AddChatWorkspaceFolderResult: Codable, Sendable { + /// The chat's working directories after the mutation. + public var directories: [String] + + public init( + directories: [String] + ) { + self.directories = directories + } +} + +public struct RemoveChatWorkspaceFolderResult: Codable, Sendable { + /// The chat's working directories after the mutation. + public var directories: [String] + + public init( + directories: [String] + ) { + self.directories = directories + } +} + public struct ListSessionsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 6c0ce0602..8f0843b03 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -187,6 +187,15 @@ public struct PartialSessionSummary: Codable, Sendable { /// `workingDirectory`}; this field acts as the fallback for any chat that /// does not. public var workingDirectory: String? + /// The full set of working directories the session's agent has tool access + /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + /// entries are equal peers — there is no privileged "primary". Individual + /// chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + /// + /// When absent, fall back to {@link workingDirectory} (if set) as a + /// single-entry set. + public var workingDirectories: [String]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -216,6 +225,7 @@ public struct PartialSessionSummary: Codable, Sendable { case activity case project case workingDirectory + case workingDirectories case annotations case resource case createdAt @@ -231,6 +241,7 @@ public struct PartialSessionSummary: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectory: String? = nil, + workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, resource: String? = nil, createdAt: String? = nil, @@ -244,6 +255,7 @@ public struct PartialSessionSummary: Codable, Sendable { self.activity = activity self.project = project self.workingDirectory = workingDirectory + self.workingDirectories = workingDirectories self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index dc9872c28..805d1d79d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -869,13 +869,19 @@ public struct ChatState: Codable, Sendable { /// compatibility. public var interactivity: ChatInteractivity? /// Optional per-chat working directory. - /// - /// If absent, the chat inherits - /// {@link SessionState.workingDirectory | the session's working directory}. - /// Hosts MAY override this for individual chats — for example, to give a - /// subordinate chat its own git worktree so multiple chats in a session can - /// make independent edits that the orchestrator later merges back. public var workingDirectory: String? + /// The subset of the session's + /// {@link SessionState.workingDirectories | `workingDirectories`} that this + /// chat's agent has tool access to. Every entry MUST be present in the owning + /// session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` + /// calls that violate this constraint. + /// + /// When absent, the chat inherits the full session set. When present but empty + /// (not recommended), the chat has no working-directory tool access at all. + /// + /// Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the + /// set on a running chat. + public var workingDirectories: [String]? /// Completed turns public var turns: [Turn] /// Cursor for loading older completed turns into this chat state. @@ -917,6 +923,7 @@ public struct ChatState: Codable, Sendable { case origin case interactivity case workingDirectory + case workingDirectories case turns case turnsNextCursor case activeTurn @@ -936,6 +943,7 @@ public struct ChatState: Codable, Sendable { origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, workingDirectory: String? = nil, + workingDirectories: [String]? = nil, turns: [Turn], turnsNextCursor: String? = nil, activeTurn: ActiveTurn? = nil, @@ -953,6 +961,7 @@ public struct ChatState: Codable, Sendable { self.origin = origin self.interactivity = interactivity self.workingDirectory = workingDirectory + self.workingDirectories = workingDirectories self.turns = turns self.turnsNextCursor = turnsNextCursor self.activeTurn = activeTurn @@ -984,11 +993,10 @@ public struct ChatSummary: Codable, Sendable { /// compatibility. public var interactivity: ChatInteractivity? /// Optional per-chat working directory. - /// - /// If absent, the chat inherits - /// {@link SessionSummary.workingDirectory | the session's working directory}. - /// See {@link ChatState.workingDirectory} for usage notes. public var workingDirectory: String? + /// The subset of the session's working directories this chat uses. + /// See {@link ChatState.workingDirectories} for the full semantics. + public var workingDirectories: [String]? public init( resource: String, @@ -998,7 +1006,8 @@ public struct ChatSummary: Codable, Sendable { modifiedAt: String, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, - workingDirectory: String? = nil + workingDirectory: String? = nil, + workingDirectories: [String]? = nil ) { self.resource = resource self.title = title @@ -1008,6 +1017,7 @@ public struct ChatSummary: Codable, Sendable { self.origin = origin self.interactivity = interactivity self.workingDirectory = workingDirectory + self.workingDirectories = workingDirectories } } @@ -1027,6 +1037,15 @@ public struct SessionState: Codable, Sendable { /// `workingDirectory`}; this field acts as the fallback for any chat that /// does not. public var workingDirectory: String? + /// The full set of working directories the session's agent has tool access + /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + /// entries are equal peers — there is no privileged "primary". Individual + /// chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + /// + /// When absent, fall back to {@link workingDirectory} (if set) as a + /// single-entry set. + public var workingDirectories: [String]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1112,6 +1131,7 @@ public struct SessionState: Codable, Sendable { case activity case project case workingDirectory + case workingDirectories case annotations case lifecycle case creationError @@ -1133,6 +1153,7 @@ public struct SessionState: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectory: String? = nil, + workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, lifecycle: SessionLifecycle, creationError: ErrorInfo? = nil, @@ -1152,6 +1173,7 @@ public struct SessionState: Codable, Sendable { self.activity = activity self.project = project self.workingDirectory = workingDirectory + self.workingDirectories = workingDirectories self.annotations = annotations self.lifecycle = lifecycle self.creationError = creationError @@ -1310,6 +1332,15 @@ public struct SessionSummary: Codable, Sendable { /// `workingDirectory`}; this field acts as the fallback for any chat that /// does not. public var workingDirectory: String? + /// The full set of working directories the session's agent has tool access + /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + /// entries are equal peers — there is no privileged "primary". Individual + /// chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + /// + /// When absent, fall back to {@link workingDirectory} (if set) as a + /// single-entry set. + public var workingDirectories: [String]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1339,6 +1370,7 @@ public struct SessionSummary: Codable, Sendable { case activity case project case workingDirectory + case workingDirectories case annotations case resource case createdAt @@ -1354,6 +1386,7 @@ public struct SessionSummary: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectory: String? = nil, + workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, resource: String, createdAt: String, @@ -1367,6 +1400,7 @@ public struct SessionSummary: Codable, Sendable { self.activity = activity self.project = project self.workingDirectory = workingDirectory + self.workingDirectories = workingDirectories self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index d6adc50c8..389427f67 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -19,7 +19,7 @@ the tag matches the version pinned in [`VERSION`](VERSION). ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). ## [0.5.2] — 2026-07-09 diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 835f00652..77082995c 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -22,7 +22,7 @@ hotfix escape hatch. ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` commands for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). ## [0.5.2] — 2026-07-09 diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 5d758baa6..de5c5461e 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -152,7 +152,8 @@ ChatState { activity?: string modifiedAt: string origin?: ChatOrigin // how the chat came to exist (user / fork / tool) - workingDirectory?: URI + workingDirectory?: URI // deprecated: use workingDirectories + workingDirectories?: URI[] // subset of session's workingDirectories turns: Turn[] // completed turns turnsNextCursor?: string // page older turns via fetchTurns @@ -600,6 +601,49 @@ Both commands return the **full directory set after the mutation** (the Before issuing either command, a client MUST verify that the agent advertises `multipleWorkspaceFolders`; servers MUST reject these commands otherwise. +### Per-chat working-directory subsets + +Each chat within a multiroot session may further restrict the directories it +uses to a **subset** of the session's `workingDirectories`. This lets +different chats in the same session each focus on a different part of the +workspace — for example, a frontend chat and a backend chat. + +The effective set for a chat is recorded in `ChatState.workingDirectories` / +`ChatSummary.workingDirectories`. When absent the chat inherits the full +session set. + +#### Setting at create time + +Pass `workingDirectories` to `createChat`. Every entry must already exist in +the session's `workingDirectories`: + +```typescript +createChat({ + channel: 'ahp-session:/', + chat: 'ahp-chat:/', + workingDirectories: ['file:///workspace/frontend'], // subset +}); +``` + +Forked chats (those with a `source`) inherit the source chat's +`workingDirectories`; the field is ignored for them. + +#### Managing the subset after creation + +Two commands mutate a running chat's working-directory subset: + +| Command | Effect | +| --- | --- | +| `addChatWorkspaceFolder` | Adds a directory to the chat's subset. The directory MUST already be in the session's `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. Adding a directory already in the chat's set is a no-op. | +| `removeChatWorkspaceFolder` | Removes a directory from the chat's subset (idempotent). | + +Both commands return the **full chat subset after the mutation** (the +`directories` field of `AddChatWorkspaceFolderResult` / +`RemoveChatWorkspaceFolderResult`). + +A client MUST NOT issue these commands unless the agent advertises +`multipleWorkspaceFolders`. + ## Next Steps - [Actions](/guide/actions) — How state is mutated. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 31a07408f..69c4d5955 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -235,7 +235,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." @@ -2708,6 +2715,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2747,6 +2761,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2990,7 +3011,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -3016,6 +3037,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -4242,7 +4270,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionState.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." }, "turns": { "type": "array", @@ -4329,7 +4364,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 9ca94d977..46ac2e2e7 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1213,6 +1213,13 @@ "source": { "$ref": "#/$defs/ChatForkSource", "description": "Optional source chat and turn to fork from." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}." } }, "required": [ @@ -1233,6 +1240,90 @@ "channel" ] }, + "AddChatWorkspaceFolderParams": { + "type": "object", + "description": "Grants this chat's agent tool access to a working directory, adding it to\nthe chat's {@link ChatState.workingDirectories | `workingDirectories`} subset.\nThe directory MUST already be present in the owning session's\n`workingDirectories`; servers MUST reject with `InvalidParams` otherwise.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`. Adding a\ndirectory already in the chat's set is a no-op that still returns the current\nfull set.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to grant tool access to. Must be in the session's `workingDirectories`." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "RemoveChatWorkspaceFolderParams": { + "type": "object", + "description": "Revokes this chat's agent tool access to one of its working directories.\nAnalogous to the session-level `removeWorkspaceFolder`: the server\nreconfigures the chat to the reduced subset and returns it. Removing a\ndirectory not in the chat's set is a no-op that still returns the current\nfull set.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to revoke tool access to." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "ChatWorkspaceFolderResult": { + "type": "object", + "description": "Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`:\nthe chat's full working-directory subset after the mutation.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The chat's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "AddChatWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The chat's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "RemoveChatWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The chat's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, "CreateTerminalParams": { "type": "object", "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", @@ -2066,6 +2157,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2105,6 +2203,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2348,7 +2453,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -2374,6 +2479,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -3600,7 +3712,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionState.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." }, "turns": { "type": "array", @@ -3687,7 +3806,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "required": [ @@ -6195,7 +6321,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." diff --git a/schema/errors.schema.json b/schema/errors.schema.json index b791efa79..75fada1fe 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -767,6 +767,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -806,6 +813,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -1049,7 +1063,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -1075,6 +1089,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2301,7 +2322,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionState.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." }, "turns": { "type": "array", @@ -2388,7 +2416,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "required": [ @@ -5874,6 +5909,13 @@ "source": { "$ref": "#/$defs/ChatForkSource", "description": "Optional source chat and turn to fork from." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}." } }, "required": [ @@ -5894,6 +5936,90 @@ "channel" ] }, + "AddChatWorkspaceFolderParams": { + "type": "object", + "description": "Grants this chat's agent tool access to a working directory, adding it to\nthe chat's {@link ChatState.workingDirectories | `workingDirectories`} subset.\nThe directory MUST already be present in the owning session's\n`workingDirectories`; servers MUST reject with `InvalidParams` otherwise.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`. Adding a\ndirectory already in the chat's set is a no-op that still returns the current\nfull set.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to grant tool access to. Must be in the session's `workingDirectories`." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "RemoveChatWorkspaceFolderParams": { + "type": "object", + "description": "Revokes this chat's agent tool access to one of its working directories.\nAnalogous to the session-level `removeWorkspaceFolder`: the server\nreconfigures the chat to the reduced subset and returns it. Removing a\ndirectory not in the chat's set is a no-op that still returns the current\nfull set.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "folder": { + "$ref": "#/$defs/URI", + "description": "Directory to revoke tool access to." + } + }, + "required": [ + "channel", + "folder" + ] + }, + "ChatWorkspaceFolderResult": { + "type": "object", + "description": "Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`:\nthe chat's full working-directory subset after the mutation.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The chat's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "AddChatWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The chat's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, + "RemoveChatWorkspaceFolderResult": { + "type": "object", + "description": "Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The chat's working directories after the mutation." + } + }, + "required": [ + "directories" + ] + }, "CreateTerminalParams": { "type": "object", "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", @@ -7129,7 +7255,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 65acea3be..b849a406b 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -102,6 +102,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -927,6 +934,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -966,6 +980,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -1209,7 +1230,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -1235,6 +1256,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2461,7 +2489,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionState.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." }, "turns": { "type": "array", @@ -2548,7 +2583,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 5d74157a1..3744d6a25 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -678,6 +678,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -717,6 +724,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -960,7 +974,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -986,6 +1000,13 @@ "$ref": "#/$defs/URI", "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2212,7 +2233,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionState.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." }, "turns": { "type": "array", @@ -2299,7 +2327,14 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + "description": "Optional per-chat working directory." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "required": [ diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 4e1dc3fc9..af0e2e8c0 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1439,6 +1439,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'AddWorkspaceFolderParams' }, { name: 'RemoveWorkspaceFolderParams' }, { name: 'WorkspaceFolderResult' }, { name: 'AddWorkspaceFolderResult' }, { name: 'RemoveWorkspaceFolderResult' }, { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' }, + { name: 'AddChatWorkspaceFolderParams' }, { name: 'RemoveChatWorkspaceFolderParams' }, + { name: 'ChatWorkspaceFolderResult' }, { name: 'AddChatWorkspaceFolderResult' }, { name: 'RemoveChatWorkspaceFolderResult' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, { name: 'ResourceReadParams' }, { name: 'ResourceReadResult' }, { name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 8a8e9714f..8d543168f 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1413,6 +1413,8 @@ const COMMAND_STRUCTS = [ 'AddWorkspaceFolderParams', 'RemoveWorkspaceFolderParams', 'WorkspaceFolderResult', 'AddWorkspaceFolderResult', 'RemoveWorkspaceFolderResult', 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', + 'AddChatWorkspaceFolderParams', 'RemoveChatWorkspaceFolderParams', + 'ChatWorkspaceFolderResult', 'AddChatWorkspaceFolderResult', 'RemoveChatWorkspaceFolderResult', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', 'ResourceWriteParams', 'ResourceWriteResult', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index bc149facd..3856a6388 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1389,6 +1389,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'WorkspaceFolderResult' }, { name: 'AddWorkspaceFolderResult' }, { name: 'RemoveWorkspaceFolderResult' }, { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' }, + { name: 'AddChatWorkspaceFolderParams' }, { name: 'RemoveChatWorkspaceFolderParams' }, + { name: 'ChatWorkspaceFolderResult' }, { name: 'AddChatWorkspaceFolderResult' }, { name: 'RemoveChatWorkspaceFolderResult' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, { name: 'ResourceReadParams' }, { name: 'ResourceReadResult' }, { name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index b36f759b3..6770cabfe 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1331,6 +1331,8 @@ const COMMAND_STRUCTS = [ 'AddWorkspaceFolderParams', 'RemoveWorkspaceFolderParams', 'WorkspaceFolderResult', 'AddWorkspaceFolderResult', 'RemoveWorkspaceFolderResult', 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', + 'AddChatWorkspaceFolderParams', 'RemoveChatWorkspaceFolderParams', + 'ChatWorkspaceFolderResult', 'AddChatWorkspaceFolderResult', 'RemoveChatWorkspaceFolderResult', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', 'ResourceWriteParams', 'ResourceWriteResult', diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index e029e2f2d..0f5da1656 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -1,5 +1,6 @@ /** - * Chat Channel Commands — `createChat` and `disposeChat`. + * Chat Channel Commands — `createChat`, `disposeChat`, + * `addChatWorkspaceFolder`, and `removeChatWorkspaceFolder`. * * @module channels-chat/commands */ @@ -38,6 +39,17 @@ export interface CreateChatParams extends BaseParams { initialMessage?: Message; /** Optional source chat and turn to fork from. */ source?: ChatForkSource; + /** + * Initial working-directory subset for this chat. Every entry MUST be + * present in the owning session's `workingDirectories`; the server MUST + * reject any entry that is not. When absent, the chat inherits the full + * session set. Forked chats (`source`) inherit the source chat's + * `workingDirectories`; this field is ignored for forked chats. + * + * A client MUST NOT supply this field unless the agent advertises + * {@link AgentCapabilities.multipleWorkspaceFolders}. + */ + workingDirectories?: URI[]; } // ─── disposeChat ───────────────────────────────────────────────────────────── @@ -52,3 +64,67 @@ export interface CreateChatParams extends BaseParams { * @version 1 */ export interface DisposeChatParams extends BaseParams {} + +// ─── addChatWorkspaceFolder ────────────────────────────────────────────────── + +/** + * Grants this chat's agent tool access to a working directory, adding it to + * the chat's {@link ChatState.workingDirectories | `workingDirectories`} subset. + * The directory MUST already be present in the owning session's + * `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. + * + * Only valid when the agent advertises `multipleWorkspaceFolders`. Adding a + * directory already in the chat's set is a no-op that still returns the current + * full set. + * + * @category Commands + * @method addChatWorkspaceFolder + * @direction Client → Server + * @messageType Request + * @version 0.6.0 + */ +export interface AddChatWorkspaceFolderParams extends BaseParams { + /** Directory to grant tool access to. Must be in the session's `workingDirectories`. */ + folder: URI; +} + +// ─── removeChatWorkspaceFolder ─────────────────────────────────────────────── + +/** + * Revokes this chat's agent tool access to one of its working directories. + * Analogous to the session-level `removeWorkspaceFolder`: the server + * reconfigures the chat to the reduced subset and returns it. Removing a + * directory not in the chat's set is a no-op that still returns the current + * full set. + * + * Only valid when the agent advertises `multipleWorkspaceFolders`. + * + * @category Commands + * @method removeChatWorkspaceFolder + * @direction Client → Server + * @messageType Request + * @version 0.6.0 + */ +export interface RemoveChatWorkspaceFolderParams extends BaseParams { + /** Directory to revoke tool access to. */ + folder: URI; +} + +/** + * Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`: + * the chat's full working-directory subset after the mutation. + */ +export interface ChatWorkspaceFolderResult { + /** The chat's working directories after the mutation. */ + directories: URI[]; +} + +/** + * Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. + */ +export interface AddChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} + +/** + * Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. + */ +export interface RemoveChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 9dff723fb..b5d053d46 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -60,13 +60,26 @@ export interface ChatState { /** * Optional per-chat working directory. * - * If absent, the chat inherits - * {@link SessionState.workingDirectory | the session's working directory}. - * Hosts MAY override this for individual chats — for example, to give a - * subordinate chat its own git worktree so multiple chats in a session can - * make independent edits that the orchestrator later merges back. + * @deprecated Use {@link workingDirectories} instead. Retained as a + * single-directory shorthand for backwards compatibility. When + * `workingDirectories` is present this field reflects its first entry so + * older clients continue to see a usable value. */ workingDirectory?: URI; + /** + * The subset of the session's + * {@link SessionState.workingDirectories | `workingDirectories`} that this + * chat's agent has tool access to. Every entry MUST be present in the owning + * session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` + * calls that violate this constraint. + * + * When absent, the chat inherits the full session set. When present but empty + * (not recommended), the chat has no working-directory tool access at all. + * + * Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the + * set on a running chat. + */ + workingDirectories?: URI[]; // ── Conversation contents ────────────────────────────────────────── /** Completed turns */ @@ -139,11 +152,16 @@ export interface ChatSummary { /** * Optional per-chat working directory. * - * If absent, the chat inherits - * {@link SessionSummary.workingDirectory | the session's working directory}. - * See {@link ChatState.workingDirectory} for usage notes. + * @deprecated Use {@link workingDirectories} instead. Retained as a + * single-directory shorthand for backwards compatibility. When + * `workingDirectories` is present this field reflects its first entry. */ workingDirectory?: URI; + /** + * The subset of the session's working directories this chat uses. + * See {@link ChatState.workingDirectories} for the full semantics. + */ + workingDirectories?: URI[]; } /** diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 1b05f0748..3aaff9075 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -87,8 +87,24 @@ export interface SessionMetadata { * MAY override via {@link ChatSummary.workingDirectory | their own * `workingDirectory`}; this field acts as the fallback for any chat that * does not. + * + * @deprecated Use {@link workingDirectories} instead. Retained as a + * single-directory shorthand for backwards compatibility. When + * `workingDirectories` is present this field reflects its first entry so + * older clients continue to see a usable value. */ workingDirectory?: URI; + /** + * The full set of working directories the session's agent has tool access + * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All + * entries are equal peers — there is no privileged "primary". Individual + * chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. + * + * When absent, fall back to {@link workingDirectory} (if set) as a + * single-entry set. + */ + workingDirectories?: URI[]; /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -387,9 +403,9 @@ export interface ProjectInfo { * chat currently driving the promoted status bits when a non-default chat * wins (e.g. the chat that raised `InputNeeded`). * - `modifiedAt`: the max of all chats' `modifiedAt`. - * - `workingDirectory`: the session-level **default**. Individual chats MAY - * override via {@link ChatSummary.workingDirectory}; aggregating these up - * is meaningless and SHOULD NOT be attempted. + * - `workingDirectory` / `workingDirectories`: the session-level set. Individual + * chats MAY restrict to a subset via {@link ChatSummary.workingDirectories}; + * aggregating these up is meaningless and SHOULD NOT be attempted. * - `changes`: optional roll-up across all chats. Producers MAY sum the * per-chat changeset stats or report the most expensive chat's stats — * whichever is cheaper for the host to compute. diff --git a/types/common/messages.ts b/types/common/messages.ts index 8daed9de2..3a9831d88 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -61,6 +61,10 @@ import type { import type { CreateChatParams, DisposeChatParams, + AddChatWorkspaceFolderParams, + AddChatWorkspaceFolderResult, + RemoveChatWorkspaceFolderParams, + RemoveChatWorkspaceFolderResult, } from '../channels-chat/commands.js'; import type { CreateTerminalParams, @@ -161,6 +165,8 @@ export interface CommandMap { 'removeWorkspaceFolder': { params: RemoveWorkspaceFolderParams; result: RemoveWorkspaceFolderResult }; 'createChat': { params: CreateChatParams; result: null }; 'disposeChat': { params: DisposeChatParams; result: null }; + 'addChatWorkspaceFolder': { params: AddChatWorkspaceFolderParams; result: AddChatWorkspaceFolderResult }; + 'removeChatWorkspaceFolder': { params: RemoveChatWorkspaceFolderParams; result: RemoveChatWorkspaceFolderResult }; 'createTerminal': { params: CreateTerminalParams; result: null }; 'disposeTerminal': { params: DisposeTerminalParams; result: null }; 'createResourceWatch': { params: CreateResourceWatchParams; result: CreateResourceWatchResult }; diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index acd5807db..38613ea5e 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -60,6 +60,8 @@ type _ExpectedCommands = | 'removeWorkspaceFolder' | 'createChat' | 'disposeChat' + | 'addChatWorkspaceFolder' + | 'removeChatWorkspaceFolder' | 'createTerminal' | 'disposeTerminal' | 'createResourceWatch' From 2bd50e6cbac77109613b9aa983fb5bd5844da5b1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 Jul 2026 17:31:45 +0200 Subject: [PATCH 03/15] spec: group multiroot changes by working directory + proposal doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional Changeset.workingDirectory (with a 'directory' changeKind hint) so a multiroot session groups its file changes by directory — advertising one changeset per working directory rather than nesting changes as arrays-of-arrays. Also add the shareable multiroot-sessions feature proposal under docs/proposals/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 + clients/go/CHANGELOG.md | 2 +- clients/go/ahptypes/state.generated.go | 13 + clients/kotlin/CHANGELOG.md | 2 +- .../generated/State.generated.kt | 15 + clients/rust/CHANGELOG.md | 2 +- clients/rust/crates/ahp-types/src/state.rs | 14 + .../Generated/State.generated.swift | 15 + clients/swift/CHANGELOG.md | 2 +- clients/typescript/CHANGELOG.md | 2 +- docs/guide/changesets.md | 15 +- docs/proposals/multiroot-sessions.md | 322 ++++++++++++++++++ schema/actions.schema.json | 6 +- schema/commands.schema.json | 6 +- schema/errors.schema.json | 6 +- schema/notifications.schema.json | 6 +- schema/state.schema.json | 6 +- types/channels-changeset/state.ts | 17 +- 18 files changed, 443 insertions(+), 13 deletions(-) create mode 100644 docs/proposals/multiroot-sessions.md diff --git a/CHANGELOG.md b/CHANGELOG.md index fbbea1e7b..5877f7ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,11 @@ Spec version: `0.6.0` - `removeChatWorkspaceFolder` command — removes a directory from a chat's working-directory subset (idempotent). Returns `RemoveChatWorkspaceFolderResult` with the chat's full subset after the mutation. +- `Changeset.workingDirectory` — an optional working directory a changeset is + scoped to (one of the session's `workingDirectories`), plus a `'directory'` + `changeKind` hint. A multiroot session groups its changes by directory by + advertising one changeset per working directory rather than nesting changes + as arrays-of-arrays. ### Deprecated diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index b11460f6c..ef7806b2a 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -16,7 +16,7 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. ## [0.5.2] — 2026-07-09 diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 3d8a95271..02e6dacc0 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -2972,6 +2972,9 @@ type Changeset struct { // // - `'session'`: a static, session-wide changeset covering all changes the // agent has produced in this session. + // - `'directory'`: all changes the agent has produced within a single + // {@link workingDirectory | working directory} of a multiroot session. + // Paired with {@link workingDirectory}. // - `'branch'`: changes relative to a base branch (e.g. a feature branch // diffed against `main`). // - `'uncommitted'`: the workspace's current uncommitted changes. @@ -2984,6 +2987,16 @@ type Changeset struct { // Implementations MAY provide additional values; clients SHOULD fall back // to a reasonable default when an unknown value is encountered. ChangeKind string `json:"changeKind"` + // The working directory this changeset is scoped to, when it covers changes + // within a single directory of a multiroot session. MUST be one of the + // session's {@link SessionState.workingDirectories}. + // + // A host with multiple working directories groups changes by directory by + // advertising one changeset per directory, each carrying its + // `workingDirectory` — rather than nesting changes as arrays-of-arrays. + // Omit for changesets that are not directory-scoped (e.g. a single-directory + // session, or a session-wide roll-up spanning every directory). + WorkingDirectory *URI `json:"workingDirectory,omitempty"` // Optional capability declarations for this changeset. Absent (or an empty // object) means the changeset advertises no optional capabilities. // diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index f7379149f..0d4ad024c 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -17,7 +17,7 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. ## [0.5.2] — 2026-07-09 diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 2b7566055..b5dbafea0 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -4039,6 +4039,9 @@ data class Changeset( * * - `'session'`: a static, session-wide changeset covering all changes the * agent has produced in this session. + * - `'directory'`: all changes the agent has produced within a single + * {@link workingDirectory | working directory} of a multiroot session. + * Paired with {@link workingDirectory}. * - `'branch'`: changes relative to a base branch (e.g. a feature branch * diffed against `main`). * - `'uncommitted'`: the workspace's current uncommitted changes. @@ -4052,6 +4055,18 @@ data class Changeset( * to a reasonable default when an unknown value is encountered. */ val changeKind: String, + /** + * The working directory this changeset is scoped to, when it covers changes + * within a single directory of a multiroot session. MUST be one of the + * session's {@link SessionState.workingDirectories}. + * + * A host with multiple working directories groups changes by directory by + * advertising one changeset per directory, each carrying its + * `workingDirectory` — rather than nesting changes as arrays-of-arrays. + * Omit for changesets that are not directory-scoped (e.g. a single-directory + * session, or a session-wide roll-up spanning every directory). + */ + val workingDirectory: String? = null, /** * Optional capability declarations for this changeset. Absent (or an empty * object) means the changeset advertises no optional capabilities. diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index b48afada8..b0faf9f24 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -17,7 +17,7 @@ matching `## [X.Y.Z]` heading is missing from this file. ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. ## [0.5.2] — 2026-07-09 diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 6bf0d7c8f..17148bcf2 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -3609,6 +3609,9 @@ pub struct Changeset { /// /// - `'session'`: a static, session-wide changeset covering all changes the /// agent has produced in this session. + /// - `'directory'`: all changes the agent has produced within a single + /// {@link workingDirectory | working directory} of a multiroot session. + /// Paired with {@link workingDirectory}. /// - `'branch'`: changes relative to a base branch (e.g. a feature branch /// diffed against `main`). /// - `'uncommitted'`: the workspace's current uncommitted changes. @@ -3621,6 +3624,17 @@ pub struct Changeset { /// Implementations MAY provide additional values; clients SHOULD fall back /// to a reasonable default when an unknown value is encountered. pub change_kind: String, + /// The working directory this changeset is scoped to, when it covers changes + /// within a single directory of a multiroot session. MUST be one of the + /// session's {@link SessionState.workingDirectories}. + /// + /// A host with multiple working directories groups changes by directory by + /// advertising one changeset per directory, each carrying its + /// `workingDirectory` — rather than nesting changes as arrays-of-arrays. + /// Omit for changesets that are not directory-scoped (e.g. a single-directory + /// session, or a session-wide roll-up spanning every directory). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directory: Option, /// Optional capability declarations for this changeset. Absent (or an empty /// object) means the changeset advertises no optional capabilities. /// diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 805d1d79d..0cad1e666 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -4505,6 +4505,9 @@ public struct Changeset: Codable, Sendable { /// /// - `'session'`: a static, session-wide changeset covering all changes the /// agent has produced in this session. + /// - `'directory'`: all changes the agent has produced within a single + /// {@link workingDirectory | working directory} of a multiroot session. + /// Paired with {@link workingDirectory}. /// - `'branch'`: changes relative to a base branch (e.g. a feature branch /// diffed against `main`). /// - `'uncommitted'`: the workspace's current uncommitted changes. @@ -4517,6 +4520,16 @@ public struct Changeset: Codable, Sendable { /// Implementations MAY provide additional values; clients SHOULD fall back /// to a reasonable default when an unknown value is encountered. public var changeKind: String + /// The working directory this changeset is scoped to, when it covers changes + /// within a single directory of a multiroot session. MUST be one of the + /// session's {@link SessionState.workingDirectories}. + /// + /// A host with multiple working directories groups changes by directory by + /// advertising one changeset per directory, each carrying its + /// `workingDirectory` — rather than nesting changes as arrays-of-arrays. + /// Omit for changesets that are not directory-scoped (e.g. a single-directory + /// session, or a session-wide roll-up spanning every directory). + public var workingDirectory: String? /// Optional capability declarations for this changeset. Absent (or an empty /// object) means the changeset advertises no optional capabilities. /// @@ -4532,12 +4545,14 @@ public struct Changeset: Codable, Sendable { uriTemplate: String, description: String? = nil, changeKind: String, + workingDirectory: String? = nil, capabilities: ChangesetCapabilities? = nil ) { self.label = label self.uriTemplate = uriTemplate self.description = description self.changeKind = changeKind + self.workingDirectory = workingDirectory self.capabilities = capabilities } } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index 389427f67..39cadb400 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -19,7 +19,7 @@ the tag matches the version pinned in [`VERSION`](VERSION). ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. ## [0.5.2] — 2026-07-09 diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 77082995c..1ef6d4384 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -22,7 +22,7 @@ hotfix escape hatch. ### Added -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). +- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. ## [0.5.2] — 2026-07-09 diff --git a/docs/guide/changesets.md b/docs/guide/changesets.md index 9d3800730..c2d79cac8 100644 --- a/docs/guide/changesets.md +++ b/docs/guide/changesets.md @@ -29,10 +29,17 @@ Changeset { uriTemplate: string description?: string /** - * Advisory hint: one of `'session'`, `'branch'`, `'uncommitted'`, - * `'turn'`, or `'compare-turns'`. Other values allowed. + * Advisory hint: one of `'session'`, `'directory'`, `'branch'`, + * `'uncommitted'`, `'turn'`, or `'compare-turns'`. Other values allowed. */ changeKind: string + /** + * Working directory this changeset is scoped to, when it covers a single + * directory of a multiroot session. One of the session's + * `workingDirectories`. Omitted for single-directory or session-wide + * changesets. + */ + workingDirectory?: URI /** Optional capability declarations (presence-flag objects). */ capabilities?: { /** Present ⇒ this changeset supports the per-file review workflow. */ @@ -55,6 +62,10 @@ unknown variables. | `{turnId}` | Per-turn slice. Expand with a `Turn.id` from one of the session's chats. | | `{originalTurnId}` and `{modifiedTurnId}` | Diff between two turns. Both must be present. | +### Multiroot Sessions + +A session with multiple [working directories](/guide/state-model#multiroot-sessions) groups its changes **by directory**: the host advertises one catalogue entry per working directory, each carrying a `workingDirectory` (one of the session's `workingDirectories`) and typically `changeKind: 'directory'`. This keeps each changeset a flat list of files rather than nesting changes as arrays-of-arrays. A host MAY additionally advertise a session-wide entry (no `workingDirectory`) that rolls up every directory. + ### Changeset State Each concrete (expanded) changeset URI is its own subscribable resource. diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md new file mode 100644 index 000000000..a285e9c82 --- /dev/null +++ b/docs/proposals/multiroot-sessions.md @@ -0,0 +1,322 @@ +# Multiroot Sessions — Feature Overview + +> A conceptual walkthrough of the multiroot-sessions feature for presentations +> and design discussion, plus a concise map of the concrete protocol surface so +> reviewers can connect the framing to the actual `types/` changes. Like the +> [multi-chat overview](./multi-chat.md), it explains **what** the capability is +> and **why** it exists before it gets to the wire shape. + +--- + +## 1. The problem + +An agent session today is scoped to **a single working directory**. The session +`workingDirectory` is the one root the agent has tool access to — the folder it +reads, edits, runs commands in, and diffs. + +That model is increasingly at odds with real tasks. Work routinely spans **more +than one directory at a time**: + +- A change to a service *and* the shared library it depends on, in sibling + repositories. +- A monorepo task that touches two packages that a user keeps as separate + checkouts. +- A migration that edits a source repo and its generated-client repo together. +- A VS Code **multi-root workspace** (`.code-workspace`) whose folders are, to + the user, one project. + +When a session can only ever see one directory, the user is forced to either +flatten everything under one root that doesn't match how they actually work, or +run **disconnected sessions** per folder that lose their shared context (the same +task, the same conversation, the same configuration). + +**The feature in one sentence:** let a single session grant its agent tool +access to *multiple working directories* — equal peers, no privileged +"primary" — so that cross-directory work can be represented and driven as one +coherent whole. + +--- + +## 2. The mental model + +Three roles, nested: + +- **A session owns a *set* of working directories.** They are **equal peers** — + there is no "primary" and no "additional." The session is the boundary of what + the agent may touch: every directory in the set is fair game, nothing outside + it is. + +- **A chat operates in a *subset* of the session's directories.** A single + thread of work usually focuses on part of the session. A chat may pin itself to + one directory (or a few); when it pins nothing, it sees the session's whole + set. + +- **Changes are grouped *by directory*.** The file changes an agent produces are + surfaced as one changeset per directory, each carrying which directory it + belongs to — a flat list per root, not a nested tangle. + +> One session, many equal directories. Each chat narrows to a subset. Changes +> group by directory. + +```mermaid +flowchart TB + S["Session
workingDirectories: [repo-a, repo-b, repo-c]
equal peers — no primary"] + S --> CA["Chat A
subset: [repo-a, repo-b]"] + S --> CB["Chat B
subset: [repo-c]"] + S --> CC["Chat C
no subset → whole set"] +``` + +A helpful analogy: the session is a **VS Code multi-root workspace**, its +directories are the **workspace folders**, and a chat is a **task** that may care +about only some of those folders. + +--- + +## 3. Where this feature lives in the stack + +As with multi-chat, this feature is a **window** onto what the harness does — not +the mechanism itself. + +``` + ┌─────────────┐ ┌──────────────────────────────┐ + │ UI client │ ◄── feature layer ──► │ Agent harness │ + │ (the app a │ directory set, per- │ ── grants the agent tool │ + │ user sees) │ chat subset, per-dir │ access to N directories │ + └─────────────┘ changesets │ ── roots the process / │ + │ applies path grants │ + └──────────────────────────────┘ +``` + +- **The harness layer** is where directory access actually takes effect: rooting + the agent process, applying filesystem path grants, isolating worktrees. How a + backend enforces "the agent may touch these three folders" is its own business. + +- **The feature/interoperability layer** (where this feature lives) lets a UI + *declare and observe* that set — pick the directories at session creation, + add/remove them later, narrow a chat to a subset, and render changes grouped by + directory. + +**The feature is about representation and control, not access enforcement.** It +gives clients a vocabulary for "this session works across these folders"; the +harness decides how that is realized. + +--- + +## 4. What the feature gives you + +At the feature level, multiroot sessions introduce a small, additive set of +capabilities: + +1. **A capability gate.** An agent advertises whether it supports more than one + working directory. Clients check it before offering any multiroot affordance; + agents that don't support it behave exactly as today. + +2. **A session directory set.** A session is created with a *list* of working + directories, all equal peers. A single-directory session is just the list of + length one. + +3. **Add / remove after start.** Directories can be granted or revoked while the + session runs. Removal is modelled as "reconfigure to this reduced set" — there + is no fragile single-remove primitive underneath. + +4. **A per-chat subset.** Each chat may narrow to a subset of the session's + directories (every entry must be one of the session's). A chat that narrows + nothing operates against the whole set. + +5. **Changes grouped by directory.** The session surfaces one changeset per + working directory, each tagged with the directory it covers — so a UI can show + "changes in repo-a / repo-b" without inventing arrays-of-arrays. + +6. **Graceful backward compatibility.** The old singular `workingDirectory` + fields are kept as a deprecated single-directory shorthand, so clients that + predate this feature keep working unchanged. + +```mermaid +flowchart LR + Cap["capability:
multipleWorkspaceFolders?"] --> Create["createSession
workingDirectories[]"] + Create --> Add["addWorkspaceFolder /
removeWorkspaceFolder"] + Create --> Chat["chat subset
workingDirectories ⊆ session"] + Create --> CS["changesets
grouped by workingDirectory"] +``` + +--- + +## 5. Worked example: a cross-repo change + +A user asks an agent to update an API and the client library that consumes it, +kept as two separate checkouts. + +| What the user / harness does | How the feature represents it | +| --- | --- | +| User starts a task over `api/` and `client/` | A session with `workingDirectories: [api, client]`. | +| Agent edits both repos | The agent has tool access to both directories, equal peers. | +| User opens a focused thread on just the client | A chat pinned to `workingDirectories: [client]`. | +| Agent later needs the shared `protos/` repo too | `addWorkspaceFolder(protos)` → set becomes `[api, client, protos]`. | +| User reviews the diff | Three changesets, one per directory, each tagged with its `workingDirectory`. | +| The `protos/` work turns out unnecessary | `removeWorkspaceFolder(protos)` → set reconfigures back to `[api, client]`. | + +The session stays one coherent conversation and one shared configuration +throughout — no juggling three disconnected sessions. + +--- + +## 6. What this feature deliberately is *not* + +- **It is not a permissions / sandboxing model.** The directory set says *which + folders the session works across*, not a fine-grained ACL of what the agent may + read vs. write. Enforcement and least-privilege policy stay a harness concern. + +- **It is not per-file or per-glob scoping.** The unit is a working directory (a + root), not arbitrary path patterns within it. + +- **It does not introduce a primary.** All directories are peers. Anything that + needs "the main one" (e.g. an old client) reads the deprecated singular field, + which is just a shorthand — not a semantic primary. + +- **It is not multi-root *chats as sub-sessions*.** A chat narrowing to a subset + is still one thread under one session's trust and identity. Independent agents + with their own lifecycle remain the separate, future sub-session concept. + +These omissions keep the feature small and composable. Richer path policy, +per-tool scoping, or workspace-level config are natural *future* axes that can +layer on without breaking this shape. + +--- + +## 7. Why this shape + +- **Equal peers, no primary.** The user's mental model ("these folders are my + project") has no privileged root. Encoding a primary would create a confusing + "what's the relationship between primary and secondary?" question with no good + answer — so the model refuses it. + +- **Session owns, chat subsets.** The broadest scope lives where it is shared + (the session); a chat only ever *narrows*, never widens. This keeps the + invariant simple: a chat's directories are always ⊆ the session's. + +- **Changes group by directory, not arrays-of-arrays.** Reusing the existing + changeset catalogue — one entry per directory, tagged with its + `workingDirectory` — keeps each changeset a flat file list and avoids a new + nested shape. + +- **Additive and capability-gated.** Every new field is optional; the commands + are gated behind a capability plus a version handshake. A single-directory + harness is untouched; a multiroot harness simply lights up more directories. + +```mermaid +flowchart LR + A["Single-dir session
(workingDirectories: [one])"] -->|same wire shape| B["Multiroot session
(workingDirectories: [N])"] + A -. looks like today .-> A + B -. folder set + per-dir diffs .-> B +``` + +--- + +## 8. Protocol surface (for reviewers) + +The concrete, additive changes that back the framing above. Full detail lives in +the [State Model](/guide/state-model#multiroot-sessions) and +[Changesets](/guide/changesets) guides; this is the reviewer's map. Target spec +version: **0.6.0**. + +**Capability** — `AgentCapabilities` + +- `multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability` — presence + (`{}`) signals support. When absent, clients MUST NOT send the add/remove + commands and MUST NOT supply more than one directory anywhere. + +**Session** — `CreateSessionParams`, `SessionMetadata` (→ `SessionState` + +`SessionSummary`) + +- `workingDirectories?: URI[]` — the session's equal-peer directory set. +- `workingDirectory?: URI` — **deprecated**; single-directory shorthand / mirror + of the first entry, retained for old clients. + +**Runtime mutation** — new commands on the session channel + +- `addWorkspaceFolder({ folder })` → `WorkspaceFolderResult { directories }` +- `removeWorkspaceFolder({ folder })` → `WorkspaceFolderResult { directories }` + (idempotent; modelled as reconfigure-to-reduced-set; a server MAY refuse a + directory it can't relinquish while live). + +**Chat** — `ChatState`, `ChatSummary`, `CreateChatParams` + +- `workingDirectories?: URI[]` — the chat's subset (every entry MUST be one of + the session's). Absent → the session's whole set. +- `workingDirectory?: URI` — **deprecated** singular shorthand. + +**Changes** — `Changeset` + +- `workingDirectory?: URI` — the directory a changeset is scoped to; plus a + `'directory'` `changeKind` hint. Multiroot sessions advertise one changeset per + directory instead of nesting changes. + +**Versioning / gating** + +- `PROTOCOL_VERSION` bumps `0.5.1 → 0.6.0` (a capability boundary). The new + commands are gated by the capability + the `initialize` version handshake, not + by the action/notification introduced-in maps (they are commands, like + `createChat`). + +--- + +## 9. Design decisions & resolved questions + +- **No primary directory.** *Resolved:* the set is equal peers. Rejected a + "primary + additional" split because it re-introduces the confusion the user + called out ("what's the relation between primary and secondary?"). + +- **Deprecate, don't remove, `workingDirectory`.** *Resolved:* the singular field + is read by every existing client from session state; removing it would break + single-directory sessions too. It stays as a deprecated shorthand. + +- **Chat narrows to a subset (not exactly one).** *Resolved:* an earlier draft + made a chat operate in exactly one directory; this was widened to "a subset ⊆ + the session's set," with absent meaning the whole set. Gated by the same + capability. + +- **Changes grouped by directory, on the changeset.** *Resolved:* put a + `workingDirectory` on `Changeset` and advertise one per directory, rather than + an array-of-arrays or a per-file `root` tag. Reuses the catalogue abstraction + and keeps each changeset flat. + +- **Removal semantics.** *Resolved:* no single-remove primitive is assumed; + `removeWorkspaceFolder` reconfigures to the reduced set and returns it, so it is + idempotent and safe to retry. + +--- + +## 10. Open questions / future axes + +- **Config-resolution context.** `resolveSessionConfig` / + `sessionConfigCompletions` still take a *singular* `workingDirectory` as the + context for resolving config (e.g. listing git branches for a worktree picker). + Whether — and how — to make config resolution multiroot-aware is left as a + follow-up. + +- **Hard removal of the singular field.** Since the spec is pre-1.0, a future + release *could* drop `workingDirectory` entirely. Kept deprecated for now to + avoid breaking existing single-directory clients; promote to removal later if + desired. + +- **Per-directory rollups on the lightweight summary.** `ChangesSummary` is a + single aggregate today. If session-list UIs want per-repo badges without + subscribing, a `byDirectory` rollup could be added later — deferred to keep the + summary lightweight. + +- **Richer path policy.** Per-tool scoping, read-vs-write per directory, or + glob-level rules are deliberately out of scope and can layer on top. + +--- + +## 11. One-slide summary + +- **Before:** a session is scoped to one `workingDirectory`. +- **After:** a session owns a **set of equal-peer directories**; a **chat** works + in a **subset**; **changes group by directory**. +- **Why:** cross-directory / multi-repo / multi-root-workspace tasks are one + coherent piece of work, not N disconnected sessions. +- **How it stays safe:** capability-gated, additive, singular field deprecated + (not removed), removal modelled as reconfigure-to-reduced-set. +- **What it is not:** not a permission model, not per-file scoping, not a primary + directory, not sub-sessions — those are separate/future axes. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 69c4d5955..f14c83551 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -6330,7 +6330,11 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 46ac2e2e7..fb8191c82 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -5772,7 +5772,11 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 75fada1fe..5b07bd0b3 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -4382,7 +4382,11 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index b849a406b..2e472c161 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -4549,7 +4549,11 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/schema/state.schema.json b/schema/state.schema.json index 3744d6a25..1ece34d7e 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -4293,7 +4293,11 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/types/channels-changeset/state.ts b/types/channels-changeset/state.ts index 58bd16d80..0aec08885 100644 --- a/types/channels-changeset/state.ts +++ b/types/channels-changeset/state.ts @@ -5,7 +5,7 @@ * @module channels-changeset/state */ -import type { StringOrMarkdown, FileEdit, ErrorInfo } from '../common/state.js'; +import type { URI, StringOrMarkdown, FileEdit, ErrorInfo } from '../common/state.js'; // ─── Changesets ────────────────────────────────────────────────────────────── @@ -50,6 +50,9 @@ export interface Changeset { * * - `'session'`: a static, session-wide changeset covering all changes the * agent has produced in this session. + * - `'directory'`: all changes the agent has produced within a single + * {@link workingDirectory | working directory} of a multiroot session. + * Paired with {@link workingDirectory}. * - `'branch'`: changes relative to a base branch (e.g. a feature branch * diffed against `main`). * - `'uncommitted'`: the workspace's current uncommitted changes. @@ -63,6 +66,18 @@ export interface Changeset { * to a reasonable default when an unknown value is encountered. */ changeKind: string; + /** + * The working directory this changeset is scoped to, when it covers changes + * within a single directory of a multiroot session. MUST be one of the + * session's {@link SessionState.workingDirectories}. + * + * A host with multiple working directories groups changes by directory by + * advertising one changeset per directory, each carrying its + * `workingDirectory` — rather than nesting changes as arrays-of-arrays. + * Omit for changesets that are not directory-scoped (e.g. a single-directory + * session, or a session-wide roll-up spanning every directory). + */ + workingDirectory?: URI; /** * Optional capability declarations for this changeset. Absent (or an empty * object) means the changeset advertises no optional capabilities. From 4bcb754ef8ef5d2fea9b9897b33c08f3f227d0c7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 Jul 2026 17:35:27 +0200 Subject: [PATCH 04/15] spec: use @version 1 for new multiroot commands (JSDoc convention) The @version JSDoc tag is a per-message revision counter (integer, starting at 1 for a brand-new message), not the protocol semver. The new multiroot commands were mistakenly tagged @version 0.6.0; align them with the convention used by every other first-revision message. Documentation-only: nothing parses @version, so no generated output changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- types/channels-chat/commands.ts | 4 ++-- types/channels-session/commands.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 0f5da1656..a23be4eb4 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -81,7 +81,7 @@ export interface DisposeChatParams extends BaseParams {} * @method addChatWorkspaceFolder * @direction Client → Server * @messageType Request - * @version 0.6.0 + * @version 1 */ export interface AddChatWorkspaceFolderParams extends BaseParams { /** Directory to grant tool access to. Must be in the session's `workingDirectories`. */ @@ -103,7 +103,7 @@ export interface AddChatWorkspaceFolderParams extends BaseParams { * @method removeChatWorkspaceFolder * @direction Client → Server * @messageType Request - * @version 0.6.0 + * @version 1 */ export interface RemoveChatWorkspaceFolderParams extends BaseParams { /** Directory to revoke tool access to. */ diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index 332960f09..d8b483040 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -153,7 +153,7 @@ export interface DisposeSessionParams extends BaseParams {} * @method addWorkspaceFolder * @direction Client → Server * @messageType Request - * @version 0.6.0 + * @version 1 */ export interface AddWorkspaceFolderParams extends BaseParams { /** Directory to grant tool access to. */ @@ -179,7 +179,7 @@ export interface AddWorkspaceFolderParams extends BaseParams { * @method removeWorkspaceFolder * @direction Client → Server * @messageType Request - * @version 0.6.0 + * @version 1 */ export interface RemoveWorkspaceFolderParams extends BaseParams { /** Directory to revoke tool access to. */ From 71d6b5f889dfce1b6e0a25b06aa3297b39921415 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 Jul 2026 17:36:22 +0200 Subject: [PATCH 05/15] spec: fix @version on ping (0.1.0 -> 1) Align the pre-existing ping command with the @version JSDoc convention (a per-message revision counter, not the protocol semver). The last message carrying a semver-shaped @version. Documentation-only; no generated output changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- types/common/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/common/commands.ts b/types/common/commands.ts index 3915776de..d68a50caf 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -277,7 +277,7 @@ export interface InitializeResult { * @method ping * @direction Client → Server * @messageType Request - * @version 0.1.0 + * @version 1 */ export interface PingParams extends BaseParams { channel: 'ahp-root://'; From 189f46ced424cd350630eaf8a10c02a1f7965b66 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 Jul 2026 17:41:16 +0200 Subject: [PATCH 06/15] docs: add type-change summary to multiroot proposal Expand the proposal's protocol-surface section with a per-symbol change table and concrete TypeScript signatures (capability, session/chat working-directory fields + deprecations, the four add/remove commands, Changeset.workingDirectory, CommandMap entries) plus a versioning/gating note, so reviewers can grasp the exact protocol delta at a glance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/proposals/multiroot-sessions.md | 155 +++++++++++++++++++-------- 1 file changed, 113 insertions(+), 42 deletions(-) diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index a285e9c82..728f4fabc 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -214,49 +214,120 @@ flowchart LR ## 8. Protocol surface (for reviewers) -The concrete, additive changes that back the framing above. Full detail lives in -the [State Model](/guide/state-model#multiroot-sessions) and -[Changesets](/guide/changesets) guides; this is the reviewer's map. Target spec -version: **0.6.0**. - -**Capability** — `AgentCapabilities` - -- `multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability` — presence - (`{}`) signals support. When absent, clients MUST NOT send the add/remove - commands and MUST NOT supply more than one directory anywhere. - -**Session** — `CreateSessionParams`, `SessionMetadata` (→ `SessionState` + -`SessionSummary`) - -- `workingDirectories?: URI[]` — the session's equal-peer directory set. -- `workingDirectory?: URI` — **deprecated**; single-directory shorthand / mirror - of the first entry, retained for old clients. - -**Runtime mutation** — new commands on the session channel - -- `addWorkspaceFolder({ folder })` → `WorkspaceFolderResult { directories }` -- `removeWorkspaceFolder({ folder })` → `WorkspaceFolderResult { directories }` - (idempotent; modelled as reconfigure-to-reduced-set; a server MAY refuse a - directory it can't relinquish while live). - -**Chat** — `ChatState`, `ChatSummary`, `CreateChatParams` - -- `workingDirectories?: URI[]` — the chat's subset (every entry MUST be one of - the session's). Absent → the session's whole set. -- `workingDirectory?: URI` — **deprecated** singular shorthand. - -**Changes** — `Changeset` - -- `workingDirectory?: URI` — the directory a changeset is scoped to; plus a - `'directory'` `changeKind` hint. Multiroot sessions advertise one changeset per - directory instead of nesting changes. - -**Versioning / gating** +The concrete, additive changes that back the framing above — a reviewer's map of +the `types/` surface. Full prose detail lives in the +[State Model](/guide/state-model#multiroot-sessions) and +[Changesets](/guide/changesets) guides. Target spec version: **0.6.0**. + +Everything here is **additive and optional** — no field is required, no existing +field changes type, and old clients that ignore the new surface behave exactly +as today. + +### 8.1 Change table + +| Symbol | File | Kind | +| --- | --- | --- | +| `AgentCapabilities.multipleWorkspaceFolders?` | `channels-root/state.ts` | added (capability) | +| `MultipleWorkspaceFoldersCapability` | `channels-root/state.ts` | added (type) | +| `CreateSessionParams.workingDirectories?` | `channels-session/commands.ts` | added | +| `CreateSessionParams.workingDirectory?` | `channels-session/commands.ts` | **deprecated** | +| `SessionMetadata.workingDirectories?` (→ `SessionState`, `SessionSummary`) | `channels-session/state.ts` | added | +| `SessionMetadata.workingDirectory?` | `channels-session/state.ts` | **deprecated** | +| `addWorkspaceFolder` / `removeWorkspaceFolder` commands | `channels-session/commands.ts` | added (command) | +| `WorkspaceFolderResult` (+ `Add…`/`Remove…Result`) | `channels-session/commands.ts` | added (type) | +| `ChatState.workingDirectories?` / `ChatSummary.workingDirectories?` | `channels-chat/state.ts` | added | +| `ChatState.workingDirectory?` / `ChatSummary.workingDirectory?` | `channels-chat/state.ts` | **deprecated** | +| `CreateChatParams.workingDirectories?` | `channels-chat/commands.ts` | added | +| `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` commands | `channels-chat/commands.ts` | added (command) | +| `ChatWorkspaceFolderResult` (+ `Add…`/`Remove…Result`) | `channels-chat/commands.ts` | added (type) | +| `Changeset.workingDirectory?` + `'directory'` `changeKind` | `channels-changeset/state.ts` | added | +| 4 command entries in `CommandMap` | `common/messages.ts` | added | + +### 8.2 Type signatures + +```ts +// ── Capability — channels-root/state.ts ────────────────────────────────── +interface AgentCapabilities { + // …existing… + /** Presence ({}) = the agent supports >1 working directory per session. */ + multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability; +} +interface MultipleWorkspaceFoldersCapability {} // presence-flag, reserved for options + +// ── Session create — channels-session/commands.ts ──────────────────────── +interface CreateSessionParams extends BaseParams { + // …existing… + /** The session's equal-peer working directories (no "primary"). */ + workingDirectories?: URI[]; + /** @deprecated single-directory shorthand; use workingDirectories. */ + workingDirectory?: URI; +} + +// ── Session state — channels-session/state.ts (→ SessionState/SessionSummary) +interface SessionMetadata { + // …existing… + workingDirectories?: URI[]; + /** @deprecated mirror of workingDirectories[0] for old clients. */ + workingDirectory?: URI; +} + +// ── Session runtime mutation — channels-session/commands.ts ─────────────── +// channel = session URI. Gated by multipleWorkspaceFolders. +interface AddWorkspaceFolderParams extends BaseParams { folder: URI; } +interface RemoveWorkspaceFolderParams extends BaseParams { folder: URI; } +interface WorkspaceFolderResult { directories: URI[]; } // full set after mutation +interface AddWorkspaceFolderResult extends WorkspaceFolderResult {} +interface RemoveWorkspaceFolderResult extends WorkspaceFolderResult {} + +// ── Chat — channels-chat/state.ts & commands.ts ────────────────────────── +interface ChatState /* and ChatSummary */ { + // …existing… + /** The chat's subset — every entry MUST be one of the session's dirs. */ + workingDirectories?: URI[]; + /** @deprecated single-directory shorthand. */ + workingDirectory?: URI; +} +interface CreateChatParams extends BaseParams { + // …existing… + workingDirectories?: URI[]; // subset ⊆ session; absent → whole session set +} +// channel = chat URI. Gated by multipleWorkspaceFolders. +interface AddChatWorkspaceFolderParams extends BaseParams { folder: URI; } +interface RemoveChatWorkspaceFolderParams extends BaseParams { folder: URI; } +interface ChatWorkspaceFolderResult { directories: URI[]; } +interface AddChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} +interface RemoveChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} + +// ── Changes — channels-changeset/state.ts ──────────────────────────────── +interface Changeset { + // …existing… + changeKind: string; // now also accepts 'directory' + /** Directory this changeset is scoped to (one of the session's dirs). */ + workingDirectory?: URI; +} + +// ── Command registry — common/messages.ts ──────────────────────────────── +interface CommandMap { + // …existing… + addWorkspaceFolder: { params: AddWorkspaceFolderParams; result: AddWorkspaceFolderResult }; + removeWorkspaceFolder: { params: RemoveWorkspaceFolderParams; result: RemoveWorkspaceFolderResult }; + addChatWorkspaceFolder: { params: AddChatWorkspaceFolderParams; result: AddChatWorkspaceFolderResult }; + removeChatWorkspaceFolder: { params: RemoveChatWorkspaceFolderParams; result: RemoveChatWorkspaceFolderResult }; +} +``` -- `PROTOCOL_VERSION` bumps `0.5.1 → 0.6.0` (a capability boundary). The new - commands are gated by the capability + the `initialize` version handshake, not - by the action/notification introduced-in maps (they are commands, like - `createChat`). +### 8.3 Versioning & gating + +- `PROTOCOL_VERSION` is **`0.6.0`** (a capability boundary; the branch's base + already advanced the ongoing-dev version to `0.6.0`). `SUPPORTED_PROTOCOL_VERSIONS` + = `[0.6.0, 0.5.2, 0.5.1]`. +- The four new commands are gated by the `multipleWorkspaceFolders` capability + **plus** the `initialize` version handshake — not by the + `ACTION_INTRODUCED_IN` / `NOTIFICATION_INTRODUCED_IN` maps (those track state + actions and server notifications; commands are gated like `createChat`). +- Removal (`removeWorkspaceFolder` / `removeChatWorkspaceFolder`) is idempotent + and modelled as *reconfigure-to-the-reduced-set*; a server MAY refuse a + directory it cannot relinquish while live. --- From 5495b7f632cb660799cf3f16d5dcaa20ed88b88f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 Jul 2026 23:34:22 +0200 Subject: [PATCH 07/15] spec: address review feedback on multiroot PR #337 - Directory mutations are now state actions, not commands: replace the four add/remove(Chat)WorkspaceFolder commands with client-dispatchable session/workingDirectorySet, session/workingDirectoryRemoved, chat/workingDirectorySet, chat/workingDirectoryRemoved actions (keyed by the directory URI, idempotent), with reducers, ACTION_INTRODUCED_IN entries (0.6.0), and full-branch conformance fixtures. - Hard-remove the deprecated singular `workingDirectory` from CreateSessionParams, SessionMetadata, ChatState, and ChatSummary (0.6.0 is a breaking release). Update hand-written client reducers/mergers in every language accordingly. - Add `immutablePrimary?` to MultipleWorkspaceFoldersCapability so backends that pin a fixed process root can advertise it while keeping equal peers the default. - Keep Changeset.workingDirectory and strengthen it to a normative MUST: a multiroot host groups changes by directory (server-side), per AHP's display-ready-state doctrine. - Move the CHANGELOG entries into a docs/.changes fragment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 53 --- clients/go/CHANGELOG.md | 4 - clients/go/ahp/reducers.go | 4 +- clients/go/ahptypes/actions.generated.go | 87 +++++ clients/go/ahptypes/commands.generated.go | 113 +------ clients/go/ahptypes/common.go | 4 +- .../go/ahptypes/notifications.generated.go | 22 +- clients/go/ahptypes/state.generated.go | 111 ++++--- clients/kotlin/CHANGELOG.md | 4 - .../microsoft/agenthostprotocol/Reducers.kt | 2 +- .../generated/Actions.generated.kt | 60 +++- .../generated/Commands.generated.kt | 108 +------ .../generated/Messages.generated.kt | 6 - .../generated/Notifications.generated.kt | 24 +- .../generated/State.generated.kt | 116 +++---- clients/rust/CHANGELOG.md | 4 - clients/rust/crates/ahp-types/src/actions.rs | 78 ++++- clients/rust/crates/ahp-types/src/commands.rs | 134 +------- .../crates/ahp-types/src/notifications.rs | 23 +- clients/rust/crates/ahp-types/src/state.rs | 121 +++---- clients/rust/crates/ahp/src/hosts/runtime.rs | 4 +- clients/rust/crates/ahp/src/reducers.rs | 10 +- .../Generated/Actions.generated.swift | 80 ++++- .../Generated/Commands.generated.swift | 140 +------- .../Generated/Messages.generated.swift | 8 - .../Generated/Notifications.generated.swift | 25 +- .../Generated/State.generated.swift | 117 +++---- .../Sources/AgentHostProtocol/Reducers.swift | 2 +- .../Hosts/HostRuntime.swift | 2 +- clients/swift/CHANGELOG.md | 4 - clients/typescript/CHANGELOG.md | 4 - .../typescript/src/client/hosts/runtime.ts | 2 +- .../.changes/20260716-multiroot-sessions.json | 5 + docs/guide/changesets.md | 6 +- docs/guide/state-model.md | 42 ++- docs/proposals/multiroot-sessions.md | 173 +++++----- schema/actions.schema.json | 135 ++++++-- schema/commands.schema.json | 302 ++++++------------ schema/errors.schema.json | 302 ++++++------------ schema/notifications.schema.json | 51 +-- schema/state.schema.json | 45 +-- scripts/generate-go.ts | 8 +- scripts/generate-kotlin.ts | 14 +- scripts/generate-rust.ts | 8 +- scripts/generate-swift.ts | 16 +- types/action-origin.generated.ts | 16 + types/channels-changeset/state.ts | 25 +- types/channels-chat/actions.ts | 43 ++- types/channels-chat/commands.ts | 67 +--- types/channels-chat/reducer.ts | 24 ++ types/channels-chat/state.ts | 25 +- types/channels-root/state.ts | 27 +- types/channels-session/actions.ts | 42 +++ types/channels-session/commands.ts | 87 +---- types/channels-session/reducer.ts | 24 ++ types/channels-session/state.ts | 37 +-- types/common/actions.ts | 12 + types/common/messages.ts | 12 - ...workingdirectoryset-adds-to-empty-set.json | 29 ++ ...gdirectoryset-appends-to-existing-set.json | 33 ++ ...rectoryset-no-op-when-already-present.json | 32 ++ ...kingdirectoryremoved-removes-existing.json | 33 ++ ...irectoryremoved-no-op-when-not-in-set.json | 32 ++ ...irectoryremoved-no-op-when-set-absent.json | 26 ++ ...workingdirectoryset-adds-to-empty-set.json | 27 ++ ...gdirectoryset-appends-to-existing-set.json | 31 ++ ...rectoryset-no-op-when-already-present.json | 30 ++ ...kingdirectoryremoved-removes-existing.json | 31 ++ ...irectoryremoved-no-op-when-not-in-set.json | 30 ++ ...irectoryremoved-no-op-when-set-absent.json | 24 ++ types/version/message-checks.ts | 4 - types/version/registry.ts | 4 + 72 files changed, 1664 insertions(+), 1726 deletions(-) create mode 100644 docs/.changes/20260716-multiroot-sessions.json create mode 100644 types/test-cases/reducers/241-session-workingdirectoryset-adds-to-empty-set.json create mode 100644 types/test-cases/reducers/242-session-workingdirectoryset-appends-to-existing-set.json create mode 100644 types/test-cases/reducers/243-session-workingdirectoryset-no-op-when-already-present.json create mode 100644 types/test-cases/reducers/244-session-workingdirectoryremoved-removes-existing.json create mode 100644 types/test-cases/reducers/245-session-workingdirectoryremoved-no-op-when-not-in-set.json create mode 100644 types/test-cases/reducers/246-session-workingdirectoryremoved-no-op-when-set-absent.json create mode 100644 types/test-cases/reducers/247-chat-workingdirectoryset-adds-to-empty-set.json create mode 100644 types/test-cases/reducers/248-chat-workingdirectoryset-appends-to-existing-set.json create mode 100644 types/test-cases/reducers/249-chat-workingdirectoryset-no-op-when-already-present.json create mode 100644 types/test-cases/reducers/250-chat-workingdirectoryremoved-removes-existing.json create mode 100644 types/test-cases/reducers/251-chat-workingdirectoryremoved-no-op-when-not-in-set.json create mode 100644 types/test-cases/reducers/252-chat-workingdirectoryremoved-no-op-when-set-absent.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5877f7ba0..fdf85ddae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,59 +27,6 @@ changes accumulate. Track in-flight protocol changes via PRs touching Spec version: `0.6.0` -### Added - -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, gating - first-class multiroot session support. When absent, clients MUST NOT call - `addWorkspaceFolder` / `removeWorkspaceFolder` / `addChatWorkspaceFolder` / - `removeChatWorkspaceFolder` and MUST NOT set more than one entry in - `CreateSessionParams.workingDirectories` or `CreateChatParams.workingDirectories`. -- `SessionMetadata.workingDirectories` (on `SessionState` and `SessionSummary`) — - the full set of working directories the session's agent has tool access to, - maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All entries are - equal peers with no privileged "primary". -- `CreateSessionParams.workingDirectories` — initial working-directory set for - the session. Ignored for forked sessions. -- `addWorkspaceFolder` command — grants a running session's agent tool access to - an additional working directory. Returns `AddWorkspaceFolderResult` carrying - the full directory set after the mutation. -- `removeWorkspaceFolder` command — revokes tool access to one working directory - by reconfiguring the agent to the reduced set. There is no atomic server-side - "remove one" primitive; the command is idempotent. Returns - `RemoveWorkspaceFolderResult` carrying the full directory set after the - mutation. -- `ChatState.workingDirectories` / `ChatSummary.workingDirectories` — the - subset of the session's `workingDirectories` that a specific chat's agent has - tool access to. When absent the chat inherits the full session set. -- `CreateChatParams.workingDirectories` — initial working-directory subset for a - new chat. Every entry must already be present in the session's - `workingDirectories`. Ignored for forked chats (inherit source chat's set). -- `addChatWorkspaceFolder` command — adds a directory to a chat's - working-directory subset (must already be in the session's set). Returns - `AddChatWorkspaceFolderResult` with the chat's full subset after the mutation. -- `removeChatWorkspaceFolder` command — removes a directory from a chat's - working-directory subset (idempotent). Returns `RemoveChatWorkspaceFolderResult` - with the chat's full subset after the mutation. -- `Changeset.workingDirectory` — an optional working directory a changeset is - scoped to (one of the session's `workingDirectories`), plus a `'directory'` - `changeKind` hint. A multiroot session groups its changes by directory by - advertising one changeset per working directory rather than nesting changes - as arrays-of-arrays. - -### Deprecated - -- `CreateSessionParams.workingDirectory` (singular) in favour of - `workingDirectories`. Retained as a single-directory shorthand for backwards - compatibility; servers MUST still honour it when `workingDirectories` is absent. -- `SessionMetadata.workingDirectory` (singular, on `SessionState` and - `SessionSummary`) in favour of `workingDirectories`. Hosts SHOULD keep - populating the singular field (set to the first entry of `workingDirectories`) - so clients that predate 0.6.0 continue to see a usable value. -- `ChatState.workingDirectory` / `ChatSummary.workingDirectory` (singular) in - favour of `workingDirectories`. Hosts SHOULD keep populating the singular - field (first entry of the chat's `workingDirectories`) for backwards - compatibility. - ## [0.5.2] — 2026-07-09 Spec version: `0.5.2` diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index ef7806b2a..6ac6b314e 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -14,10 +14,6 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] -### Added - -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. - ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 4c9cdb18d..08fbf8432 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -726,8 +726,8 @@ func mergeChatSummaryPartial(summary *ahptypes.ChatSummary, changes ahptypes.Par if changes.Origin != nil { summary.Origin = changes.Origin } - if changes.WorkingDirectory != nil { - summary.WorkingDirectory = changes.WorkingDirectory + if changes.WorkingDirectories != nil { + summary.WorkingDirectories = changes.WorkingDirectories } } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 18de3b4f5..c8a0e6db7 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -41,12 +41,16 @@ const ( ActionTypeChatTurnCancelled ActionType = "chat/turnCancelled" ActionTypeChatError ActionType = "chat/error" ActionTypeChatActivityChanged ActionType = "chat/activityChanged" + ActionTypeChatWorkingDirectorySet ActionType = "chat/workingDirectorySet" + ActionTypeChatWorkingDirectoryRemoved ActionType = "chat/workingDirectoryRemoved" ActionTypeSessionTitleChanged ActionType = "session/titleChanged" ActionTypeChatUsage ActionType = "chat/usage" ActionTypeChatReasoning ActionType = "chat/reasoning" ActionTypeSessionServerToolsChanged ActionType = "session/serverToolsChanged" ActionTypeSessionActiveClientSet ActionType = "session/activeClientSet" ActionTypeSessionActiveClientRemoved ActionType = "session/activeClientRemoved" + ActionTypeSessionWorkingDirectorySet ActionType = "session/workingDirectorySet" + ActionTypeSessionWorkingDirectoryRemoved ActionType = "session/workingDirectoryRemoved" ActionTypeSessionInputNeededSet ActionType = "session/inputNeededSet" ActionTypeSessionInputNeededRemoved ActionType = "session/inputNeededRemoved" ActionTypeChatPendingMessageSet ActionType = "chat/pendingMessageSet" @@ -802,6 +806,61 @@ type SessionActiveClientRemovedAction struct { ClientId string `json:"clientId"` } +// A working directory was added to the session's +// {@link SessionState.workingDirectories} set. +// +// Membership semantics keyed by the directory URI: the reducer appends +// `directory` when the set does not already contain it (creating the set if +// absent) and is a no-op when it is already present. Only valid when the agent +// advertises {@link AgentCapabilities.multipleWorkspaceFolders}. +type SessionWorkingDirectorySetAction struct { + Type ActionType `json:"type"` + // The working directory to grant the session's agent tool access to. + Directory URI `json:"directory"` +} + +// A working directory was removed from the session's +// {@link SessionState.workingDirectories} set. +// +// Removes `directory` from the set; a no-op when it is not present. There is no +// atomic backend "remove one" primitive — a host reconfigures its agent to the +// reduced set — so this action is safe to model as idempotent. A host MAY +// decline to apply the removal (e.g. an immutable primary directory, see +// {@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves +// the set unchanged. +type SessionWorkingDirectoryRemovedAction struct { + Type ActionType `json:"type"` + // The working directory to revoke the session's agent tool access to. + Directory URI `json:"directory"` +} + +// A working directory was added to this chat's +// {@link ChatState.workingDirectories} subset. +// +// Membership semantics keyed by the directory URI: the reducer appends +// `directory` when the chat's subset does not already contain it (creating the +// subset if absent) and is a no-op when it is already present. `directory` MUST +// be one of the owning session's {@link SessionState.workingDirectories}; a host +// MUST reject a directory that is not. Only valid when the agent advertises +// {@link AgentCapabilities.multipleWorkspaceFolders}. +type ChatWorkingDirectorySetAction struct { + Type ActionType `json:"type"` + // The working directory to add to this chat's subset. + Directory URI `json:"directory"` +} + +// A working directory was removed from this chat's +// {@link ChatState.workingDirectories} subset. +// +// Removes `directory` from the chat's subset; a no-op when it is not present. +// Idempotent, mirroring `session/workingDirectoryRemoved`. Only affects the +// chat's subset — the directory remains in the session's set. +type ChatWorkingDirectoryRemovedAction struct { + Type ActionType `json:"type"` + // The working directory to remove from this chat's subset. + Directory URI `json:"directory"` +} + // A session-level input request was added or updated. // // Upsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the @@ -1395,6 +1454,10 @@ func (*SessionChangesetsChangedAction) isStateAction() {} func (*SessionServerToolsChangedAction) isStateAction() {} func (*SessionActiveClientSetAction) isStateAction() {} func (*SessionActiveClientRemovedAction) isStateAction() {} +func (*SessionWorkingDirectorySetAction) isStateAction() {} +func (*SessionWorkingDirectoryRemovedAction) isStateAction() {} +func (*ChatWorkingDirectorySetAction) isStateAction() {} +func (*ChatWorkingDirectoryRemovedAction) isStateAction() {} func (*SessionInputNeededSetAction) isStateAction() {} func (*SessionInputNeededRemovedAction) isStateAction() {} func (*SessionCustomizationsChangedAction) isStateAction() {} @@ -1699,6 +1762,30 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "session/workingDirectorySet": + var value SessionWorkingDirectorySetAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "session/workingDirectoryRemoved": + var value SessionWorkingDirectoryRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "chat/workingDirectorySet": + var value ChatWorkingDirectorySetAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "chat/workingDirectoryRemoved": + var value ChatWorkingDirectoryRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value case "session/inputNeededSet": var value SessionInputNeededSetAction if err := json.Unmarshal(data, &value); err != nil { diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 4a86ad529..82904ac57 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -306,19 +306,21 @@ type CreateSessionParams struct { // Agent provider ID Provider *string `json:"provider,omitempty"` // The working directories the session's agent is granted tool access to. - // A session may span multiple directories, all of which are equal peers — - // there is no privileged "primary" directory. + // A session may span multiple directories; they are equal peers except when + // the agent advertises + // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + // the first entry is a fixed process root). // // A client MUST NOT supply more than one entry unless the agent advertises // {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that // capability treats only the first entry as the session's working directory - // and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` - // to change the set after the session has started. + // and ignores the rest. Dispatch `session/workingDirectorySet` / + // `session/workingDirectoryRemoved` to change the set after the session has + // started. // // Ignored for forked sessions — a fork inherits its working directories // from the source session identified by `fork`. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - WorkingDirectory *URI `json:"workingDirectory,omitempty"` // Fork from an existing session. The new session is populated with content // from the source session up to and including the specified turn's response. Fork *SessionForkSource `json:"fork,omitempty"` @@ -353,59 +355,6 @@ type DisposeSessionParams struct { Channel URI `json:"channel"` } -// Grants the session's agent tool access to a working directory, adding it to -// the session's set of {@link CreateSessionParams.workingDirectories | working -// directories}. Only valid when the agent advertises -// `multipleWorkspaceFolders`; servers MUST reject this command otherwise. -// -// All directories in the set are equal peers — there is no privileged -// "primary". Adding a directory that is already in the set is a no-op that -// still returns the current full set. -type AddWorkspaceFolderParams struct { - // Channel URI this command targets. - Channel URI `json:"channel"` - // Directory to grant tool access to. - Folder URI `json:"folder"` -} - -// Revokes the session's agent tool access to one of its working directories. -// There is no server-side primitive to "remove" a single directory mid-session -// — the server instead reconfigures the agent with the reduced directory set -// and returns it, so this command is safe to model as idempotent: removing a -// directory that is not in the set is a no-op that still returns the current -// full set. -// -// A server MAY refuse to remove a directory it cannot relinquish while the -// session is live (for example one bound to the running agent process), -// returning an error rather than a result. The protocol itself designates no -// directory as special. -type RemoveWorkspaceFolderParams struct { - // Channel URI this command targets. - Channel URI `json:"channel"` - // Directory to revoke tool access to. - Folder URI `json:"folder"` -} - -// Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder` -// commands: the session's full set of working directories after the mutation. -// All entries are equal peers; the order carries no meaning. -type WorkspaceFolderResult struct { - // The session's working directories after the mutation. - Directories []URI `json:"directories"` -} - -// Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}. -type AddWorkspaceFolderResult struct { - // The session's working directories after the mutation. - Directories []URI `json:"directories"` -} - -// Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}. -type RemoveWorkspaceFolderResult struct { - // The session's working directories after the mutation. - Directories []URI `json:"directories"` -} - // Identifies a source chat and turn to fork from. type ChatForkSource struct { // URI of the existing chat to fork from @@ -441,54 +390,6 @@ type DisposeChatParams struct { Channel URI `json:"channel"` } -// Grants this chat's agent tool access to a working directory, adding it to -// the chat's {@link ChatState.workingDirectories | `workingDirectories`} subset. -// The directory MUST already be present in the owning session's -// `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. -// -// Only valid when the agent advertises `multipleWorkspaceFolders`. Adding a -// directory already in the chat's set is a no-op that still returns the current -// full set. -type AddChatWorkspaceFolderParams struct { - // Channel URI this command targets. - Channel URI `json:"channel"` - // Directory to grant tool access to. Must be in the session's `workingDirectories`. - Folder URI `json:"folder"` -} - -// Revokes this chat's agent tool access to one of its working directories. -// Analogous to the session-level `removeWorkspaceFolder`: the server -// reconfigures the chat to the reduced subset and returns it. Removing a -// directory not in the chat's set is a no-op that still returns the current -// full set. -// -// Only valid when the agent advertises `multipleWorkspaceFolders`. -type RemoveChatWorkspaceFolderParams struct { - // Channel URI this command targets. - Channel URI `json:"channel"` - // Directory to revoke tool access to. - Folder URI `json:"folder"` -} - -// Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`: -// the chat's full working-directory subset after the mutation. -type ChatWorkspaceFolderResult struct { - // The chat's working directories after the mutation. - Directories []URI `json:"directories"` -} - -// Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. -type AddChatWorkspaceFolderResult struct { - // The chat's working directories after the mutation. - Directories []URI `json:"directories"` -} - -// Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. -type RemoveChatWorkspaceFolderResult struct { - // The chat's working directories after the mutation. - Directories []URI `json:"directories"` -} - // Returns a list of session summaries. Used to populate session lists and sidebars. // // The session list is **not** part of the state tree because it can be arbitrarily diff --git a/clients/go/ahptypes/common.go b/clients/go/ahptypes/common.go index 83ed98b32..96792a2ac 100644 --- a/clients/go/ahptypes/common.go +++ b/clients/go/ahptypes/common.go @@ -83,8 +83,8 @@ type PartialChatSummary struct { ModifiedAt *string `json:"modifiedAt,omitempty"` // How this chat came into existence Origin *ChatOrigin `json:"origin,omitempty"` - // Optional per-chat working directory. - WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // The subset of the session's working directories this chat uses. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` } // ─── StringOrMarkdown ──────────────────────────────────────────────────── diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 8268b992a..6c7cb0197 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -217,19 +217,15 @@ type PartialSessionSummary struct { Activity *string `json:"activity,omitempty"` // Server-owned project for this session Project *ProjectInfo `json:"project,omitempty"` - // The default working directory URI for this session. Individual chats - // MAY override via {@link ChatSummary.workingDirectory | their own - // `workingDirectory`}; this field acts as the fallback for any chat that - // does not. - WorkingDirectory *URI `json:"workingDirectory,omitempty"` - // The full set of working directories the session's agent has tool access - // to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - // entries are equal peers — there is no privileged "primary". Individual - // chats MAY restrict to a subset via - // {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - // - // When absent, fall back to {@link workingDirectory} (if set) as a - // single-entry set. + // The working directories the session's agent has tool access to, as + // maintained by the `session/workingDirectorySet` / + // `session/workingDirectoryRemoved` actions. Directories are equal peers + // except when the agent advertises + // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + // entry is then a fixed process root). Individual chats MAY restrict to a + // subset via {@link ChatSummary.workingDirectories | their own + // `workingDirectories`}; a chat that sets none operates against this full + // set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 02e6dacc0..db4c3efed 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -564,9 +564,12 @@ type AgentCapabilities struct { // forking; set {@link MultipleChatsCapability.fork} to also allow forking. MultipleChats *MultipleChatsCapability `json:"multipleChats,omitempty"` // The session's agent can be granted tool access to more than one working - // directory, with all directories treated as equal peers (no primary). When - // absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` - // and MUST NOT set more than one entry in + // directory. The directories are treated as equal peers except where the + // agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + // (some backends pin their first directory as a fixed process root). + // + // When absent, clients MUST NOT mutate a session's or chat's working-directory + // set and MUST NOT set more than one entry in // {@link CreateSessionParams.workingDirectories}. MultipleWorkspaceFolders *MultipleWorkspaceFoldersCapability `json:"multipleWorkspaceFolders,omitempty"` } @@ -580,9 +583,18 @@ type MultipleChatsCapability struct { } // Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. -// Currently carries no sub-options; presence of an empty object `{}` is the -// entire signal. type MultipleWorkspaceFoldersCapability struct { + // The agent's **first** working directory (index `0` of + // {@link CreateSessionParams.workingDirectories}) is an immutable primary: + // it is fixed for the lifetime of the session — clients MUST NOT remove or + // reorder it. Additional directories after it remain equal peers that can be + // added and removed freely. + // + // Advertised by backends whose agent process is rooted at a single directory + // that cannot change once the session has started (e.g. the SDK's primary + // `workingDirectory`). When absent or `false`, all directories are equal + // peers and any of them may be removed. + ImmutablePrimary *bool `json:"immutablePrimary,omitempty"` } type SessionModelInfo struct { @@ -705,19 +717,15 @@ type SessionState struct { Activity *string `json:"activity,omitempty"` // Server-owned project for this session Project *ProjectInfo `json:"project,omitempty"` - // The default working directory URI for this session. Individual chats - // MAY override via {@link ChatSummary.workingDirectory | their own - // `workingDirectory`}; this field acts as the fallback for any chat that - // does not. - WorkingDirectory *URI `json:"workingDirectory,omitempty"` - // The full set of working directories the session's agent has tool access - // to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - // entries are equal peers — there is no privileged "primary". Individual - // chats MAY restrict to a subset via - // {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - // - // When absent, fall back to {@link workingDirectory} (if set) as a - // single-entry set. + // The working directories the session's agent has tool access to, as + // maintained by the `session/workingDirectorySet` / + // `session/workingDirectoryRemoved` actions. Directories are equal peers + // except when the agent advertises + // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + // entry is then a fixed process root). Individual chats MAY restrict to a + // subset via {@link ChatSummary.workingDirectories | their own + // `workingDirectories`}; a chat that sets none operates against this full + // set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render @@ -794,7 +802,7 @@ type SessionState struct { // // Clients MAY look for well-known keys here to provide enhanced UI. // For example, a `git` key may provide extra git metadata about the session's - // workingDirectory. + // working directories. Meta map[string]json.RawMessage `json:"_meta,omitempty"` } @@ -923,9 +931,9 @@ type SessionToolClientExecutionRequest struct { // chat currently driving the promoted status bits when a non-default chat // wins (e.g. the chat that raised `InputNeeded`). // - `modifiedAt`: the max of all chats' `modifiedAt`. -// - `workingDirectory` / `workingDirectories`: the session-level set. Individual -// chats MAY restrict to a subset via {@link ChatSummary.workingDirectories}; -// aggregating these up is meaningless and SHOULD NOT be attempted. +// - `workingDirectories`: the session-level set. Individual chats MAY restrict +// to a subset via {@link ChatSummary.workingDirectories}; aggregating these +// up is meaningless and SHOULD NOT be attempted. // - `changes`: optional roll-up across all chats. Producers MAY sum the // per-chat changeset stats or report the most expensive chat's stats — // whichever is cheaper for the host to compute. @@ -944,19 +952,15 @@ type SessionSummary struct { Activity *string `json:"activity,omitempty"` // Server-owned project for this session Project *ProjectInfo `json:"project,omitempty"` - // The default working directory URI for this session. Individual chats - // MAY override via {@link ChatSummary.workingDirectory | their own - // `workingDirectory`}; this field acts as the fallback for any chat that - // does not. - WorkingDirectory *URI `json:"workingDirectory,omitempty"` - // The full set of working directories the session's agent has tool access - // to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - // entries are equal peers — there is no privileged "primary". Individual - // chats MAY restrict to a subset via - // {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - // - // When absent, fall back to {@link workingDirectory} (if set) as a - // single-entry set. + // The working directories the session's agent has tool access to, as + // maintained by the `session/workingDirectorySet` / + // `session/workingDirectoryRemoved` actions. Directories are equal peers + // except when the agent advertises + // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + // entry is then a fixed process root). Individual chats MAY restrict to a + // subset via {@link ChatSummary.workingDirectories | their own + // `workingDirectories`}; a chat that sets none operates against this full + // set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1024,19 +1028,17 @@ type ChatState struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` - // Optional per-chat working directory. - WorkingDirectory *URI `json:"workingDirectory,omitempty"` // The subset of the session's // {@link SessionState.workingDirectories | `workingDirectories`} that this // chat's agent has tool access to. Every entry MUST be present in the owning - // session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` - // calls that violate this constraint. + // session's `workingDirectories`; servers MUST reject a + // `chat/workingDirectorySet` action that violates this constraint. // // When absent, the chat inherits the full session set. When present but empty // (not recommended), the chat has no working-directory tool access at all. // - // Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the - // set on a running chat. + // Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to + // update the subset on a running chat. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Completed turns Turns []Turn `json:"turns"` @@ -1093,8 +1095,6 @@ type ChatSummary struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` - // Optional per-chat working directory. - WorkingDirectory *URI `json:"workingDirectory,omitempty"` // The subset of the session's working directories this chat uses. // See {@link ChatState.workingDirectories} for the full semantics. WorkingDirectories []URI `json:"workingDirectories,omitempty"` @@ -2987,15 +2987,24 @@ type Changeset struct { // Implementations MAY provide additional values; clients SHOULD fall back // to a reasonable default when an unknown value is encountered. ChangeKind string `json:"changeKind"` - // The working directory this changeset is scoped to, when it covers changes - // within a single directory of a multiroot session. MUST be one of the - // session's {@link SessionState.workingDirectories}. + // The working directory this changeset is scoped to. When set, it MUST be one + // of the owning session's {@link SessionState.workingDirectories}, and every + // file in the changeset belongs to that directory. + // + // **Grouping is the host's responsibility, not the client's.** A host whose + // session has multiple working directories MUST group changes by directory — + // emitting one changeset per working directory, each carrying its + // `workingDirectory` — so that a client never has to derive a file's owning + // directory itself (e.g. by prefix-matching URIs, which the client cannot do + // correctly across nested repositories, symlinks, or submodules). This keeps + // AHP a display-ready presentation model (see the + // {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS + // knowledge and hands clients pre-grouped changesets. // - // A host with multiple working directories groups changes by directory by - // advertising one changeset per directory, each carrying its - // `workingDirectory` — rather than nesting changes as arrays-of-arrays. - // Omit for changesets that are not directory-scoped (e.g. a single-directory - // session, or a session-wide roll-up spanning every directory). + // Omit only for changesets that are genuinely not scoped to a single working + // directory — e.g. a single-directory session (nothing to group), or an + // aggregate roll-up / out-of-tree changeset that intentionally spans (or sits + // outside) the working directories. WorkingDirectory *URI `json:"workingDirectory,omitempty"` // Optional capability declarations for this changeset. Absent (or an empty // object) means the changeset advertises no optional capabilities. diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 0d4ad024c..831496149 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -15,10 +15,6 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump ## [Unreleased] -### Added - -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. - ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index 712a688ea..627820a36 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -515,7 +515,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat activity = c.activity ?: prior.activity, modifiedAt = c.modifiedAt ?: prior.modifiedAt, origin = c.origin ?: prior.origin, - workingDirectory = c.workingDirectory ?: prior.workingDirectory, + workingDirectories = c.workingDirectories ?: prior.workingDirectories, ) val updated = state.chats.toMutableList() updated[idx] = updatedSummary diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index 89b7e3402..c891f68e2 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -70,6 +70,10 @@ enum class ActionType { CHAT_ERROR, @SerialName("chat/activityChanged") CHAT_ACTIVITY_CHANGED, + @SerialName("chat/workingDirectorySet") + CHAT_WORKING_DIRECTORY_SET, + @SerialName("chat/workingDirectoryRemoved") + CHAT_WORKING_DIRECTORY_REMOVED, @SerialName("session/titleChanged") SESSION_TITLE_CHANGED, @SerialName("chat/usage") @@ -82,6 +86,10 @@ enum class ActionType { SESSION_ACTIVE_CLIENT_SET, @SerialName("session/activeClientRemoved") SESSION_ACTIVE_CLIENT_REMOVED, + @SerialName("session/workingDirectorySet") + SESSION_WORKING_DIRECTORY_SET, + @SerialName("session/workingDirectoryRemoved") + SESSION_WORKING_DIRECTORY_REMOVED, @SerialName("session/inputNeededSet") SESSION_INPUT_NEEDED_SET, @SerialName("session/inputNeededRemoved") @@ -824,6 +832,42 @@ data class SessionActiveClientRemovedAction( val clientId: String ) +@Serializable +data class SessionWorkingDirectorySetAction( + val type: ActionType, + /** + * The working directory to grant the session's agent tool access to. + */ + val directory: String +) + +@Serializable +data class SessionWorkingDirectoryRemovedAction( + val type: ActionType, + /** + * The working directory to revoke the session's agent tool access to. + */ + val directory: String +) + +@Serializable +data class ChatWorkingDirectorySetAction( + val type: ActionType, + /** + * The working directory to add to this chat's subset. + */ + val directory: String +) + +@Serializable +data class ChatWorkingDirectoryRemovedAction( + val type: ActionType, + /** + * The working directory to remove from this chat's subset. + */ + val directory: String +) + @Serializable data class SessionInputNeededSetAction( val type: ActionType, @@ -1404,10 +1448,6 @@ data class PartialChatSummary( * compatibility. */ val interactivity: ChatInteractivity? = null, - /** - * Optional per-chat working directory. - */ - val workingDirectory: String? = null, /** * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. @@ -1461,6 +1501,10 @@ sealed interface StateAction @JvmInline value class StateActionSessionServerToolsChanged(val value: SessionServerToolsChangedAction) : StateAction @JvmInline value class StateActionSessionActiveClientSet(val value: SessionActiveClientSetAction) : StateAction @JvmInline value class StateActionSessionActiveClientRemoved(val value: SessionActiveClientRemovedAction) : StateAction +@JvmInline value class StateActionSessionWorkingDirectorySet(val value: SessionWorkingDirectorySetAction) : StateAction +@JvmInline value class StateActionSessionWorkingDirectoryRemoved(val value: SessionWorkingDirectoryRemovedAction) : StateAction +@JvmInline value class StateActionChatWorkingDirectorySet(val value: ChatWorkingDirectorySetAction) : StateAction +@JvmInline value class StateActionChatWorkingDirectoryRemoved(val value: ChatWorkingDirectoryRemovedAction) : StateAction @JvmInline value class StateActionSessionInputNeededSet(val value: SessionInputNeededSetAction) : StateAction @JvmInline value class StateActionSessionInputNeededRemoved(val value: SessionInputNeededRemovedAction) : StateAction @JvmInline value class StateActionChatPendingMessageSet(val value: ChatPendingMessageSetAction) : StateAction @@ -1555,6 +1599,10 @@ internal object StateActionSerializer : KSerializer { "session/serverToolsChanged" -> StateActionSessionServerToolsChanged(input.json.decodeFromJsonElement(SessionServerToolsChangedAction.serializer(), element)) "session/activeClientSet" -> StateActionSessionActiveClientSet(input.json.decodeFromJsonElement(SessionActiveClientSetAction.serializer(), element)) "session/activeClientRemoved" -> StateActionSessionActiveClientRemoved(input.json.decodeFromJsonElement(SessionActiveClientRemovedAction.serializer(), element)) + "session/workingDirectorySet" -> StateActionSessionWorkingDirectorySet(input.json.decodeFromJsonElement(SessionWorkingDirectorySetAction.serializer(), element)) + "session/workingDirectoryRemoved" -> StateActionSessionWorkingDirectoryRemoved(input.json.decodeFromJsonElement(SessionWorkingDirectoryRemovedAction.serializer(), element)) + "chat/workingDirectorySet" -> StateActionChatWorkingDirectorySet(input.json.decodeFromJsonElement(ChatWorkingDirectorySetAction.serializer(), element)) + "chat/workingDirectoryRemoved" -> StateActionChatWorkingDirectoryRemoved(input.json.decodeFromJsonElement(ChatWorkingDirectoryRemovedAction.serializer(), element)) "session/inputNeededSet" -> StateActionSessionInputNeededSet(input.json.decodeFromJsonElement(SessionInputNeededSetAction.serializer(), element)) "session/inputNeededRemoved" -> StateActionSessionInputNeededRemoved(input.json.decodeFromJsonElement(SessionInputNeededRemovedAction.serializer(), element)) "chat/pendingMessageSet" -> StateActionChatPendingMessageSet(input.json.decodeFromJsonElement(ChatPendingMessageSetAction.serializer(), element)) @@ -1642,6 +1690,10 @@ internal object StateActionSerializer : KSerializer { is StateActionSessionServerToolsChanged -> output.json.encodeToJsonElement(SessionServerToolsChangedAction.serializer(), value.value) is StateActionSessionActiveClientSet -> output.json.encodeToJsonElement(SessionActiveClientSetAction.serializer(), value.value) is StateActionSessionActiveClientRemoved -> output.json.encodeToJsonElement(SessionActiveClientRemovedAction.serializer(), value.value) + is StateActionSessionWorkingDirectorySet -> output.json.encodeToJsonElement(SessionWorkingDirectorySetAction.serializer(), value.value) + is StateActionSessionWorkingDirectoryRemoved -> output.json.encodeToJsonElement(SessionWorkingDirectoryRemovedAction.serializer(), value.value) + is StateActionChatWorkingDirectorySet -> output.json.encodeToJsonElement(ChatWorkingDirectorySetAction.serializer(), value.value) + is StateActionChatWorkingDirectoryRemoved -> output.json.encodeToJsonElement(ChatWorkingDirectoryRemovedAction.serializer(), value.value) is StateActionSessionInputNeededSet -> output.json.encodeToJsonElement(SessionInputNeededSetAction.serializer(), value.value) is StateActionSessionInputNeededRemoved -> output.json.encodeToJsonElement(SessionInputNeededRemovedAction.serializer(), value.value) is StateActionChatPendingMessageSet -> output.json.encodeToJsonElement(ChatPendingMessageSetAction.serializer(), value.value) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index da787df7c..6de973e0e 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -370,20 +370,22 @@ data class CreateSessionParams( val provider: String? = null, /** * The working directories the session's agent is granted tool access to. - * A session may span multiple directories, all of which are equal peers — - * there is no privileged "primary" directory. + * A session may span multiple directories; they are equal peers except when + * the agent advertises + * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + * the first entry is a fixed process root). * * A client MUST NOT supply more than one entry unless the agent advertises * {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that * capability treats only the first entry as the session's working directory - * and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` - * to change the set after the session has started. + * and ignores the rest. Dispatch `session/workingDirectorySet` / + * `session/workingDirectoryRemoved` to change the set after the session has + * started. * * Ignored for forked sessions — a fork inherits its working directories * from the source session identified by `fork`. */ val workingDirectories: List? = null, - val workingDirectory: String? = null, /** * Fork from an existing session. The new session is populated with content * from the source session up to and including the specified turn's response. @@ -426,54 +428,6 @@ data class DisposeSessionParams( val channel: String ) -@Serializable -data class AddWorkspaceFolderParams( - /** - * Channel URI this command targets. - */ - val channel: String, - /** - * Directory to grant tool access to. - */ - val folder: String -) - -@Serializable -data class RemoveWorkspaceFolderParams( - /** - * Channel URI this command targets. - */ - val channel: String, - /** - * Directory to revoke tool access to. - */ - val folder: String -) - -@Serializable -data class WorkspaceFolderResult( - /** - * The session's working directories after the mutation. - */ - val directories: List -) - -@Serializable -data class AddWorkspaceFolderResult( - /** - * The session's working directories after the mutation. - */ - val directories: List -) - -@Serializable -data class RemoveWorkspaceFolderResult( - /** - * The session's working directories after the mutation. - */ - val directories: List -) - @Serializable data class ChatForkSource( /** @@ -525,54 +479,6 @@ data class DisposeChatParams( val channel: String ) -@Serializable -data class AddChatWorkspaceFolderParams( - /** - * Channel URI this command targets. - */ - val channel: String, - /** - * Directory to grant tool access to. Must be in the session's `workingDirectories`. - */ - val folder: String -) - -@Serializable -data class RemoveChatWorkspaceFolderParams( - /** - * Channel URI this command targets. - */ - val channel: String, - /** - * Directory to revoke tool access to. - */ - val folder: String -) - -@Serializable -data class ChatWorkspaceFolderResult( - /** - * The chat's working directories after the mutation. - */ - val directories: List -) - -@Serializable -data class AddChatWorkspaceFolderResult( - /** - * The chat's working directories after the mutation. - */ - val directories: List -) - -@Serializable -data class RemoveChatWorkspaceFolderResult( - /** - * The chat's working directories after the mutation. - */ - val directories: List -) - @Serializable data class ListSessionsParams( /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt index 22033a54a..07ed4755c 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt @@ -98,12 +98,6 @@ object AhpCommands { fun disposeSession(id: Long, params: DisposeSessionParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "disposeSession", params = params) - fun addWorkspaceFolder(id: Long, params: AddWorkspaceFolderParams): JsonRpcRequest = - JsonRpcRequest(id = id, method = "addWorkspaceFolder", params = params) - - fun removeWorkspaceFolder(id: Long, params: RemoveWorkspaceFolderParams): JsonRpcRequest = - JsonRpcRequest(id = id, method = "removeWorkspaceFolder", params = params) - fun listSessions(id: Long, params: ListSessionsParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "listSessions", params = params) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index d2a1779b3..36bb50da6 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -196,21 +196,15 @@ data class PartialSessionSummary( */ val project: ProjectInfo? = null, /** - * The default working directory URI for this session. Individual chats - * MAY override via {@link ChatSummary.workingDirectory | their own - * `workingDirectory`}; this field acts as the fallback for any chat that - * does not. - */ - val workingDirectory: String? = null, - /** - * The full set of working directories the session's agent has tool access - * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - * entries are equal peers — there is no privileged "primary". Individual - * chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - * - * When absent, fall back to {@link workingDirectory} (if set) as a - * single-entry set. + * The working directories the session's agent has tool access to, as + * maintained by the `session/workingDirectorySet` / + * `session/workingDirectoryRemoved` actions. Directories are equal peers + * except when the agent advertises + * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * entry is then a fixed process root). Individual chats MAY restrict to a + * subset via {@link ChatSummary.workingDirectories | their own + * `workingDirectories`}; a chat that sets none operates against this full + * set. */ val workingDirectories: List? = null, /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index b5dbafea0..333a2ce01 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -923,9 +923,12 @@ data class AgentCapabilities( val multipleChats: MultipleChatsCapability? = null, /** * The session's agent can be granted tool access to more than one working - * directory, with all directories treated as equal peers (no primary). When - * absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` - * and MUST NOT set more than one entry in + * directory. The directories are treated as equal peers except where the + * agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + * (some backends pin their first directory as a fixed process root). + * + * When absent, clients MUST NOT mutate a session's or chat's working-directory + * set and MUST NOT set more than one entry in * {@link CreateSessionParams.workingDirectories}. */ val multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? = null @@ -942,7 +945,21 @@ data class MultipleChatsCapability( ) @Serializable -class MultipleWorkspaceFoldersCapability +data class MultipleWorkspaceFoldersCapability( + /** + * The agent's **first** working directory (index `0` of + * {@link CreateSessionParams.workingDirectories}) is an immutable primary: + * it is fixed for the lifetime of the session — clients MUST NOT remove or + * reorder it. Additional directories after it remain equal peers that can be + * added and removed freely. + * + * Advertised by backends whose agent process is rooted at a single directory + * that cannot change once the session has started (e.g. the SDK's primary + * `workingDirectory`). When absent or `false`, all directories are equal + * peers and any of them may be removed. + */ + val immutablePrimary: Boolean? = null +) @Serializable data class SessionModelInfo( @@ -1130,22 +1147,18 @@ data class ChatState( * compatibility. */ val interactivity: ChatInteractivity? = null, - /** - * Optional per-chat working directory. - */ - val workingDirectory: String? = null, /** * The subset of the session's * {@link SessionState.workingDirectories | `workingDirectories`} that this * chat's agent has tool access to. Every entry MUST be present in the owning - * session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` - * calls that violate this constraint. + * session's `workingDirectories`; servers MUST reject a + * `chat/workingDirectorySet` action that violates this constraint. * * When absent, the chat inherits the full session set. When present but empty * (not recommended), the chat has no working-directory tool access at all. * - * Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the - * set on a running chat. + * Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to + * update the subset on a running chat. */ val workingDirectories: List? = null, /** @@ -1232,10 +1245,6 @@ data class ChatSummary( * compatibility. */ val interactivity: ChatInteractivity? = null, - /** - * Optional per-chat working directory. - */ - val workingDirectory: String? = null, /** * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. @@ -1266,21 +1275,15 @@ data class SessionState( */ val project: ProjectInfo? = null, /** - * The default working directory URI for this session. Individual chats - * MAY override via {@link ChatSummary.workingDirectory | their own - * `workingDirectory`}; this field acts as the fallback for any chat that - * does not. - */ - val workingDirectory: String? = null, - /** - * The full set of working directories the session's agent has tool access - * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - * entries are equal peers — there is no privileged "primary". Individual - * chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - * - * When absent, fall back to {@link workingDirectory} (if set) as a - * single-entry set. + * The working directories the session's agent has tool access to, as + * maintained by the `session/workingDirectorySet` / + * `session/workingDirectoryRemoved` actions. Directories are equal peers + * except when the agent advertises + * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * entry is then a fixed process root). Individual chats MAY restrict to a + * subset via {@link ChatSummary.workingDirectories | their own + * `workingDirectories`}; a chat that sets none operates against this full + * set. */ val workingDirectories: List? = null, /** @@ -1381,7 +1384,7 @@ data class SessionState( * * Clients MAY look for well-known keys here to provide enhanced UI. * For example, a `git` key may provide extra git metadata about the session's - * workingDirectory. + * working directories. */ @SerialName("_meta") val meta: Map? = null @@ -1519,21 +1522,15 @@ data class SessionSummary( */ val project: ProjectInfo? = null, /** - * The default working directory URI for this session. Individual chats - * MAY override via {@link ChatSummary.workingDirectory | their own - * `workingDirectory`}; this field acts as the fallback for any chat that - * does not. - */ - val workingDirectory: String? = null, - /** - * The full set of working directories the session's agent has tool access - * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - * entries are equal peers — there is no privileged "primary". Individual - * chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - * - * When absent, fall back to {@link workingDirectory} (if set) as a - * single-entry set. + * The working directories the session's agent has tool access to, as + * maintained by the `session/workingDirectorySet` / + * `session/workingDirectoryRemoved` actions. Directories are equal peers + * except when the agent advertises + * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * entry is then a fixed process root). Individual chats MAY restrict to a + * subset via {@link ChatSummary.workingDirectories | their own + * `workingDirectories`}; a chat that sets none operates against this full + * set. */ val workingDirectories: List? = null, /** @@ -4056,15 +4053,24 @@ data class Changeset( */ val changeKind: String, /** - * The working directory this changeset is scoped to, when it covers changes - * within a single directory of a multiroot session. MUST be one of the - * session's {@link SessionState.workingDirectories}. + * The working directory this changeset is scoped to. When set, it MUST be one + * of the owning session's {@link SessionState.workingDirectories}, and every + * file in the changeset belongs to that directory. + * + * **Grouping is the host's responsibility, not the client's.** A host whose + * session has multiple working directories MUST group changes by directory — + * emitting one changeset per working directory, each carrying its + * `workingDirectory` — so that a client never has to derive a file's owning + * directory itself (e.g. by prefix-matching URIs, which the client cannot do + * correctly across nested repositories, symlinks, or submodules). This keeps + * AHP a display-ready presentation model (see the + * {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS + * knowledge and hands clients pre-grouped changesets. * - * A host with multiple working directories groups changes by directory by - * advertising one changeset per directory, each carrying its - * `workingDirectory` — rather than nesting changes as arrays-of-arrays. - * Omit for changesets that are not directory-scoped (e.g. a single-directory - * session, or a session-wide roll-up spanning every directory). + * Omit only for changesets that are genuinely not scoped to a single working + * directory — e.g. a single-directory session (nothing to group), or an + * aggregate roll-up / out-of-tree changeset that intentionally spans (or sits + * outside) the working directories. */ val workingDirectory: String? = null, /** diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index b0faf9f24..0d80fff39 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -15,10 +15,6 @@ matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] -### Added - -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. - ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index ba03df425..4b7008570 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -71,6 +71,10 @@ pub enum ActionType { ChatError, #[serde(rename = "chat/activityChanged")] ChatActivityChanged, + #[serde(rename = "chat/workingDirectorySet")] + ChatWorkingDirectorySet, + #[serde(rename = "chat/workingDirectoryRemoved")] + ChatWorkingDirectoryRemoved, #[serde(rename = "session/titleChanged")] SessionTitleChanged, #[serde(rename = "chat/usage")] @@ -83,6 +87,10 @@ pub enum ActionType { SessionActiveClientSet, #[serde(rename = "session/activeClientRemoved")] SessionActiveClientRemoved, + #[serde(rename = "session/workingDirectorySet")] + SessionWorkingDirectorySet, + #[serde(rename = "session/workingDirectoryRemoved")] + SessionWorkingDirectoryRemoved, #[serde(rename = "session/inputNeededSet")] SessionInputNeededSet, #[serde(rename = "session/inputNeededRemoved")] @@ -846,6 +854,65 @@ pub struct SessionActiveClientRemovedAction { pub client_id: String, } +/// A working directory was added to the session's +/// {@link SessionState.workingDirectories} set. +/// +/// Membership semantics keyed by the directory URI: the reducer appends +/// `directory` when the set does not already contain it (creating the set if +/// absent) and is a no-op when it is already present. Only valid when the agent +/// advertises {@link AgentCapabilities.multipleWorkspaceFolders}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionWorkingDirectorySetAction { + /// The working directory to grant the session's agent tool access to. + pub directory: Uri, +} + +/// A working directory was removed from the session's +/// {@link SessionState.workingDirectories} set. +/// +/// Removes `directory` from the set; a no-op when it is not present. There is no +/// atomic backend "remove one" primitive — a host reconfigures its agent to the +/// reduced set — so this action is safe to model as idempotent. A host MAY +/// decline to apply the removal (e.g. an immutable primary directory, see +/// {@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves +/// the set unchanged. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionWorkingDirectoryRemovedAction { + /// The working directory to revoke the session's agent tool access to. + pub directory: Uri, +} + +/// A working directory was added to this chat's +/// {@link ChatState.workingDirectories} subset. +/// +/// Membership semantics keyed by the directory URI: the reducer appends +/// `directory` when the chat's subset does not already contain it (creating the +/// subset if absent) and is a no-op when it is already present. `directory` MUST +/// be one of the owning session's {@link SessionState.workingDirectories}; a host +/// MUST reject a directory that is not. Only valid when the agent advertises +/// {@link AgentCapabilities.multipleWorkspaceFolders}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatWorkingDirectorySetAction { + /// The working directory to add to this chat's subset. + pub directory: Uri, +} + +/// A working directory was removed from this chat's +/// {@link ChatState.workingDirectories} subset. +/// +/// Removes `directory` from the chat's subset; a no-op when it is not present. +/// Idempotent, mirroring `session/workingDirectoryRemoved`. Only affects the +/// chat's subset — the directory remains in the session's set. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatWorkingDirectoryRemovedAction { + /// The working directory to remove from this chat's subset. + pub directory: Uri, +} + /// A session-level input request was added or updated. /// /// Upsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the @@ -1610,9 +1677,6 @@ pub struct PartialChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, - /// Optional per-chat working directory. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1691,6 +1755,14 @@ pub enum StateAction { SessionActiveClientSet(SessionActiveClientSetAction), #[serde(rename = "session/activeClientRemoved")] SessionActiveClientRemoved(SessionActiveClientRemovedAction), + #[serde(rename = "session/workingDirectorySet")] + SessionWorkingDirectorySet(SessionWorkingDirectorySetAction), + #[serde(rename = "session/workingDirectoryRemoved")] + SessionWorkingDirectoryRemoved(SessionWorkingDirectoryRemovedAction), + #[serde(rename = "chat/workingDirectorySet")] + ChatWorkingDirectorySet(ChatWorkingDirectorySetAction), + #[serde(rename = "chat/workingDirectoryRemoved")] + ChatWorkingDirectoryRemoved(ChatWorkingDirectoryRemovedAction), #[serde(rename = "session/inputNeededSet")] SessionInputNeededSet(Box), #[serde(rename = "session/inputNeededRemoved")] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index b3d0f4524..11b9c2a22 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -392,21 +392,22 @@ pub struct CreateSessionParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, /// The working directories the session's agent is granted tool access to. - /// A session may span multiple directories, all of which are equal peers — - /// there is no privileged "primary" directory. + /// A session may span multiple directories; they are equal peers except when + /// the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + /// the first entry is a fixed process root). /// /// A client MUST NOT supply more than one entry unless the agent advertises /// {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that /// capability treats only the first entry as the session's working directory - /// and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` - /// to change the set after the session has started. + /// and ignores the rest. Dispatch `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` to change the set after the session has + /// started. /// /// Ignored for forked sessions — a fork inherits its working directories /// from the source session identified by `fork`. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, /// Fork from an existing session. The new session is populated with content /// from the source session up to and including the specified turn's response. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -447,69 +448,6 @@ pub struct DisposeSessionParams { pub channel: Uri, } -/// Grants the session's agent tool access to a working directory, adding it to -/// the session's set of {@link CreateSessionParams.workingDirectories | working -/// directories}. Only valid when the agent advertises -/// `multipleWorkspaceFolders`; servers MUST reject this command otherwise. -/// -/// All directories in the set are equal peers — there is no privileged -/// "primary". Adding a directory that is already in the set is a no-op that -/// still returns the current full set. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AddWorkspaceFolderParams { - /// Channel URI this command targets. - pub channel: Uri, - /// Directory to grant tool access to. - pub folder: Uri, -} - -/// Revokes the session's agent tool access to one of its working directories. -/// There is no server-side primitive to "remove" a single directory mid-session -/// — the server instead reconfigures the agent with the reduced directory set -/// and returns it, so this command is safe to model as idempotent: removing a -/// directory that is not in the set is a no-op that still returns the current -/// full set. -/// -/// A server MAY refuse to remove a directory it cannot relinquish while the -/// session is live (for example one bound to the running agent process), -/// returning an error rather than a result. The protocol itself designates no -/// directory as special. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoveWorkspaceFolderParams { - /// Channel URI this command targets. - pub channel: Uri, - /// Directory to revoke tool access to. - pub folder: Uri, -} - -/// Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder` -/// commands: the session's full set of working directories after the mutation. -/// All entries are equal peers; the order carries no meaning. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WorkspaceFolderResult { - /// The session's working directories after the mutation. - pub directories: Vec, -} - -/// Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AddWorkspaceFolderResult { - /// The session's working directories after the mutation. - pub directories: Vec, -} - -/// Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoveWorkspaceFolderResult { - /// The session's working directories after the mutation. - pub directories: Vec, -} - /// Identifies a source chat and turn to fork from. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -554,64 +492,6 @@ pub struct DisposeChatParams { pub channel: Uri, } -/// Grants this chat's agent tool access to a working directory, adding it to -/// the chat's {@link ChatState.workingDirectories | `workingDirectories`} subset. -/// The directory MUST already be present in the owning session's -/// `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. -/// -/// Only valid when the agent advertises `multipleWorkspaceFolders`. Adding a -/// directory already in the chat's set is a no-op that still returns the current -/// full set. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AddChatWorkspaceFolderParams { - /// Channel URI this command targets. - pub channel: Uri, - /// Directory to grant tool access to. Must be in the session's `workingDirectories`. - pub folder: Uri, -} - -/// Revokes this chat's agent tool access to one of its working directories. -/// Analogous to the session-level `removeWorkspaceFolder`: the server -/// reconfigures the chat to the reduced subset and returns it. Removing a -/// directory not in the chat's set is a no-op that still returns the current -/// full set. -/// -/// Only valid when the agent advertises `multipleWorkspaceFolders`. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoveChatWorkspaceFolderParams { - /// Channel URI this command targets. - pub channel: Uri, - /// Directory to revoke tool access to. - pub folder: Uri, -} - -/// Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`: -/// the chat's full working-directory subset after the mutation. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChatWorkspaceFolderResult { - /// The chat's working directories after the mutation. - pub directories: Vec, -} - -/// Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AddChatWorkspaceFolderResult { - /// The chat's working directories after the mutation. - pub directories: Vec, -} - -/// Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoveChatWorkspaceFolderResult { - /// The chat's working directories after the mutation. - pub directories: Vec, -} - /// Returns a list of session summaries. Used to populate session lists and sidebars. /// /// The session list is **not** part of the state tree because it can be arbitrarily diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index b78bdd69d..92b154b9e 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -248,20 +248,15 @@ pub struct PartialSessionSummary { /// Server-owned project for this session #[serde(default, skip_serializing_if = "Option::is_none")] pub project: Option, - /// The default working directory URI for this session. Individual chats - /// MAY override via {@link ChatSummary.workingDirectory | their own - /// `workingDirectory`}; this field acts as the fallback for any chat that - /// does not. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, - /// The full set of working directories the session's agent has tool access - /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - /// entries are equal peers — there is no privileged "primary". Individual - /// chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - /// - /// When absent, fall back to {@link workingDirectory} (if set) as a - /// single-entry set. + /// The working directories the session's agent has tool access to, as + /// maintained by the `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` actions. Directories are equal peers + /// except when the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// entry is then a fixed process root). Individual chats MAY restrict to a + /// subset via {@link ChatSummary.workingDirectories | their own + /// `workingDirectories`}; a chat that sets none operates against this full + /// set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, /// Lightweight summary of this session's inline annotations channel diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 17148bcf2..adc2064de 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -780,9 +780,12 @@ pub struct AgentCapabilities { #[serde(default, skip_serializing_if = "Option::is_none")] pub multiple_chats: Option, /// The session's agent can be granted tool access to more than one working - /// directory, with all directories treated as equal peers (no primary). When - /// absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` - /// and MUST NOT set more than one entry in + /// directory. The directories are treated as equal peers except where the + /// agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + /// (some backends pin their first directory as a fixed process root). + /// + /// When absent, clients MUST NOT mutate a session's or chat's working-directory + /// set and MUST NOT set more than one entry in /// {@link CreateSessionParams.workingDirectories}. #[serde(default, skip_serializing_if = "Option::is_none")] pub multiple_workspace_folders: Option, @@ -800,11 +803,22 @@ pub struct MultipleChatsCapability { } /// Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. -/// Currently carries no sub-options; presence of an empty object `{}` is the -/// entire signal. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub struct MultipleWorkspaceFoldersCapability {} +pub struct MultipleWorkspaceFoldersCapability { + /// The agent's **first** working directory (index `0` of + /// {@link CreateSessionParams.workingDirectories}) is an immutable primary: + /// it is fixed for the lifetime of the session — clients MUST NOT remove or + /// reorder it. Additional directories after it remain equal peers that can be + /// added and removed freely. + /// + /// Advertised by backends whose agent process is rooted at a single directory + /// that cannot change once the session has started (e.g. the SDK's primary + /// `workingDirectory`). When absent or `false`, all directories are equal + /// peers and any of them may be removed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub immutable_primary: Option, +} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -986,20 +1000,17 @@ pub struct ChatState { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, - /// Optional per-chat working directory. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, /// The subset of the session's /// {@link SessionState.workingDirectories | `workingDirectories`} that this /// chat's agent has tool access to. Every entry MUST be present in the owning - /// session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` - /// calls that violate this constraint. + /// session's `workingDirectories`; servers MUST reject a + /// `chat/workingDirectorySet` action that violates this constraint. /// /// When absent, the chat inherits the full session set. When present but empty /// (not recommended), the chat has no working-directory tool access at all. /// - /// Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the - /// set on a running chat. + /// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to + /// update the subset on a running chat. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, /// Completed turns @@ -1069,9 +1080,6 @@ pub struct ChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, - /// Optional per-chat working directory. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1100,20 +1108,15 @@ pub struct SessionState { /// Server-owned project for this session #[serde(default, skip_serializing_if = "Option::is_none")] pub project: Option, - /// The default working directory URI for this session. Individual chats - /// MAY override via {@link ChatSummary.workingDirectory | their own - /// `workingDirectory`}; this field acts as the fallback for any chat that - /// does not. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, - /// The full set of working directories the session's agent has tool access - /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - /// entries are equal peers — there is no privileged "primary". Individual - /// chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - /// - /// When absent, fall back to {@link workingDirectory} (if set) as a - /// single-entry set. + /// The working directories the session's agent has tool access to, as + /// maintained by the `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` actions. Directories are equal peers + /// except when the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// entry is then a fixed process root). Individual chats MAY restrict to a + /// subset via {@link ChatSummary.workingDirectories | their own + /// `workingDirectories`}; a chat that sets none operates against this full + /// set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, /// Lightweight summary of this session's inline annotations channel @@ -1199,7 +1202,7 @@ pub struct SessionState { /// /// Clients MAY look for well-known keys here to provide enhanced UI. /// For example, a `git` key may provide extra git metadata about the session's - /// workingDirectory. + /// working directories. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } @@ -1336,9 +1339,9 @@ pub struct SessionToolClientExecutionRequest { /// chat currently driving the promoted status bits when a non-default chat /// wins (e.g. the chat that raised `InputNeeded`). /// - `modifiedAt`: the max of all chats' `modifiedAt`. -/// - `workingDirectory` / `workingDirectories`: the session-level set. Individual -/// chats MAY restrict to a subset via {@link ChatSummary.workingDirectories}; -/// aggregating these up is meaningless and SHOULD NOT be attempted. +/// - `workingDirectories`: the session-level set. Individual chats MAY restrict +/// to a subset via {@link ChatSummary.workingDirectories}; aggregating these +/// up is meaningless and SHOULD NOT be attempted. /// - `changes`: optional roll-up across all chats. Producers MAY sum the /// per-chat changeset stats or report the most expensive chat's stats — /// whichever is cheaper for the host to compute. @@ -1361,20 +1364,15 @@ pub struct SessionSummary { /// Server-owned project for this session #[serde(default, skip_serializing_if = "Option::is_none")] pub project: Option, - /// The default working directory URI for this session. Individual chats - /// MAY override via {@link ChatSummary.workingDirectory | their own - /// `workingDirectory`}; this field acts as the fallback for any chat that - /// does not. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, - /// The full set of working directories the session's agent has tool access - /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - /// entries are equal peers — there is no privileged "primary". Individual - /// chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - /// - /// When absent, fall back to {@link workingDirectory} (if set) as a - /// single-entry set. + /// The working directories the session's agent has tool access to, as + /// maintained by the `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` actions. Directories are equal peers + /// except when the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// entry is then a fixed process root). Individual chats MAY restrict to a + /// subset via {@link ChatSummary.workingDirectories | their own + /// `workingDirectories`}; a chat that sets none operates against this full + /// set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, /// Lightweight summary of this session's inline annotations channel @@ -3624,15 +3622,24 @@ pub struct Changeset { /// Implementations MAY provide additional values; clients SHOULD fall back /// to a reasonable default when an unknown value is encountered. pub change_kind: String, - /// The working directory this changeset is scoped to, when it covers changes - /// within a single directory of a multiroot session. MUST be one of the - /// session's {@link SessionState.workingDirectories}. + /// The working directory this changeset is scoped to. When set, it MUST be one + /// of the owning session's {@link SessionState.workingDirectories}, and every + /// file in the changeset belongs to that directory. + /// + /// **Grouping is the host's responsibility, not the client's.** A host whose + /// session has multiple working directories MUST group changes by directory — + /// emitting one changeset per working directory, each carrying its + /// `workingDirectory` — so that a client never has to derive a file's owning + /// directory itself (e.g. by prefix-matching URIs, which the client cannot do + /// correctly across nested repositories, symlinks, or submodules). This keeps + /// AHP a display-ready presentation model (see the + /// {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS + /// knowledge and hands clients pre-grouped changesets. /// - /// A host with multiple working directories groups changes by directory by - /// advertising one changeset per directory, each carrying its - /// `workingDirectory` — rather than nesting changes as arrays-of-arrays. - /// Omit for changesets that are not directory-scoped (e.g. a single-directory - /// session, or a session-wide roll-up spanning every directory). + /// Omit only for changesets that are genuinely not scoped to a single working + /// directory — e.g. a single-directory session (nothing to group), or an + /// aggregate roll-up / out-of-tree changeset that intentionally spans (or sits + /// outside) the working directories. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, /// Optional capability declarations for this changeset. Absent (or an empty diff --git a/clients/rust/crates/ahp/src/hosts/runtime.rs b/clients/rust/crates/ahp/src/hosts/runtime.rs index ecd066475..35216671f 100644 --- a/clients/rust/crates/ahp/src/hosts/runtime.rs +++ b/clients/rust/crates/ahp/src/hosts/runtime.rs @@ -724,8 +724,8 @@ fn apply_summary_changes( if let Some(v) = &changes.project { existing.project = Some(v.clone()); } - if let Some(v) = &changes.working_directory { - existing.working_directory = Some(v.clone()); + if let Some(v) = &changes.working_directories { + existing.working_directories = Some(v.clone()); } if let Some(v) = &changes.annotations { existing.annotations = Some(v.clone()); diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index ba975d70b..ad531815e 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -627,8 +627,8 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - if let Some(origin) = &a.changes.origin { chat.origin = Some(origin.clone()); } - if let Some(working_directory) = &a.changes.working_directory { - chat.working_directory = Some(working_directory.clone()); + if let Some(working_directories) = &a.changes.working_directories { + chat.working_directories = Some(working_directories.clone()); } ReduceOutcome::Applied } @@ -1742,7 +1742,7 @@ mod tests { status: SessionStatus::Idle.bits(), activity: None, project: None, - working_directory: None, + working_directories: None, annotations: None, lifecycle: SessionLifecycle::Creating, creation_error: None, @@ -1767,7 +1767,7 @@ mod tests { modified_at: "1970-01-01T00:00:00.000Z".into(), origin: None, interactivity: None, - working_directory: None, + working_directories: None, turns: Vec::new(), turns_next_cursor: None, active_turn: None, @@ -1864,7 +1864,7 @@ mod tests { modified_at: "1970-01-01T00:00:00.000Z".into(), origin: None, interactivity: None, - working_directory: None, + working_directories: None, }; let added = StateAction::SessionChatAdded(ahp_types::actions::SessionChatAddedAction { summary: chat.clone(), diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index a43c6e454..bb6648175 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -28,12 +28,16 @@ public enum ActionType: String, Codable, Sendable { case chatTurnCancelled = "chat/turnCancelled" case chatError = "chat/error" case chatActivityChanged = "chat/activityChanged" + case chatWorkingDirectorySet = "chat/workingDirectorySet" + case chatWorkingDirectoryRemoved = "chat/workingDirectoryRemoved" case sessionTitleChanged = "session/titleChanged" case chatUsage = "chat/usage" case chatReasoning = "chat/reasoning" case sessionServerToolsChanged = "session/serverToolsChanged" case sessionActiveClientSet = "session/activeClientSet" case sessionActiveClientRemoved = "session/activeClientRemoved" + case sessionWorkingDirectorySet = "session/workingDirectorySet" + case sessionWorkingDirectoryRemoved = "session/workingDirectoryRemoved" case sessionInputNeededSet = "session/inputNeededSet" case sessionInputNeededRemoved = "session/inputNeededRemoved" case chatPendingMessageSet = "chat/pendingMessageSet" @@ -1032,6 +1036,62 @@ public struct SessionActiveClientRemovedAction: Codable, Sendable { } } +public struct SessionWorkingDirectorySetAction: Codable, Sendable { + public var type: ActionType + /// The working directory to grant the session's agent tool access to. + public var directory: String + + public init( + type: ActionType, + directory: String + ) { + self.type = type + self.directory = directory + } +} + +public struct SessionWorkingDirectoryRemovedAction: Codable, Sendable { + public var type: ActionType + /// The working directory to revoke the session's agent tool access to. + public var directory: String + + public init( + type: ActionType, + directory: String + ) { + self.type = type + self.directory = directory + } +} + +public struct ChatWorkingDirectorySetAction: Codable, Sendable { + public var type: ActionType + /// The working directory to add to this chat's subset. + public var directory: String + + public init( + type: ActionType, + directory: String + ) { + self.type = type + self.directory = directory + } +} + +public struct ChatWorkingDirectoryRemovedAction: Codable, Sendable { + public var type: ActionType + /// The working directory to remove from this chat's subset. + public var directory: String + + public init( + type: ActionType, + directory: String + ) { + self.type = type + self.directory = directory + } +} + public struct SessionInputNeededSetAction: Codable, Sendable { public var type: ActionType /// The input request to add or update, matched by `id`. @@ -1836,8 +1896,6 @@ public struct PartialChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? - /// Optional per-chat working directory. - public var workingDirectory: String? /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. public var workingDirectories: [String]? @@ -1850,7 +1908,6 @@ public struct PartialChatSummary: Codable, Sendable { modifiedAt: String? = nil, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, - workingDirectory: String? = nil, workingDirectories: [String]? = nil ) { self.resource = resource @@ -1860,7 +1917,6 @@ public struct PartialChatSummary: Codable, Sendable { self.modifiedAt = modifiedAt self.origin = origin self.interactivity = interactivity - self.workingDirectory = workingDirectory self.workingDirectories = workingDirectories } } @@ -1901,6 +1957,10 @@ public enum StateAction: Codable, Sendable { case sessionServerToolsChanged(SessionServerToolsChangedAction) case sessionActiveClientSet(SessionActiveClientSetAction) case sessionActiveClientRemoved(SessionActiveClientRemovedAction) + case sessionWorkingDirectorySet(SessionWorkingDirectorySetAction) + case sessionWorkingDirectoryRemoved(SessionWorkingDirectoryRemovedAction) + case chatWorkingDirectorySet(ChatWorkingDirectorySetAction) + case chatWorkingDirectoryRemoved(ChatWorkingDirectoryRemovedAction) case sessionInputNeededSet(SessionInputNeededSetAction) case sessionInputNeededRemoved(SessionInputNeededRemovedAction) case chatPendingMessageSet(ChatPendingMessageSetAction) @@ -2024,6 +2084,14 @@ public enum StateAction: Codable, Sendable { self = .sessionActiveClientSet(try SessionActiveClientSetAction(from: decoder)) case "session/activeClientRemoved": self = .sessionActiveClientRemoved(try SessionActiveClientRemovedAction(from: decoder)) + case "session/workingDirectorySet": + self = .sessionWorkingDirectorySet(try SessionWorkingDirectorySetAction(from: decoder)) + case "session/workingDirectoryRemoved": + self = .sessionWorkingDirectoryRemoved(try SessionWorkingDirectoryRemovedAction(from: decoder)) + case "chat/workingDirectorySet": + self = .chatWorkingDirectorySet(try ChatWorkingDirectorySetAction(from: decoder)) + case "chat/workingDirectoryRemoved": + self = .chatWorkingDirectoryRemoved(try ChatWorkingDirectoryRemovedAction(from: decoder)) case "session/inputNeededSet": self = .sessionInputNeededSet(try SessionInputNeededSetAction(from: decoder)) case "session/inputNeededRemoved": @@ -2157,6 +2225,10 @@ public enum StateAction: Codable, Sendable { case .sessionServerToolsChanged(let v): try v.encode(to: encoder) case .sessionActiveClientSet(let v): try v.encode(to: encoder) case .sessionActiveClientRemoved(let v): try v.encode(to: encoder) + case .sessionWorkingDirectorySet(let v): try v.encode(to: encoder) + case .sessionWorkingDirectoryRemoved(let v): try v.encode(to: encoder) + case .chatWorkingDirectorySet(let v): try v.encode(to: encoder) + case .chatWorkingDirectoryRemoved(let v): try v.encode(to: encoder) case .sessionInputNeededSet(let v): try v.encode(to: encoder) case .sessionInputNeededRemoved(let v): try v.encode(to: encoder) case .chatPendingMessageSet(let v): try v.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index acafb5b97..f3c63ca92 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -355,19 +355,21 @@ public struct CreateSessionParams: Codable, Sendable { /// Agent provider ID public var provider: String? /// The working directories the session's agent is granted tool access to. - /// A session may span multiple directories, all of which are equal peers — - /// there is no privileged "primary" directory. + /// A session may span multiple directories; they are equal peers except when + /// the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + /// the first entry is a fixed process root). /// /// A client MUST NOT supply more than one entry unless the agent advertises /// {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that /// capability treats only the first entry as the session's working directory - /// and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` - /// to change the set after the session has started. + /// and ignores the rest. Dispatch `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` to change the set after the session has + /// started. /// /// Ignored for forked sessions — a fork inherits its working directories /// from the source session identified by `fork`. public var workingDirectories: [String]? - public var workingDirectory: String? /// Fork from an existing session. The new session is populated with content /// from the source session up to and including the specified turn's response. public var fork: SessionForkSource? @@ -397,7 +399,6 @@ public struct CreateSessionParams: Codable, Sendable { channel: String, provider: String? = nil, workingDirectories: [String]? = nil, - workingDirectory: String? = nil, fork: SessionForkSource? = nil, config: [String: AnyCodable]? = nil, activeClient: SessionActiveClient? = nil, @@ -406,7 +407,6 @@ public struct CreateSessionParams: Codable, Sendable { self.channel = channel self.provider = provider self.workingDirectories = workingDirectories - self.workingDirectory = workingDirectory self.fork = fork self.config = config self.activeClient = activeClient @@ -425,69 +425,6 @@ public struct DisposeSessionParams: Codable, Sendable { } } -public struct AddWorkspaceFolderParams: Codable, Sendable { - /// Channel URI this command targets. - public var channel: String - /// Directory to grant tool access to. - public var folder: String - - public init( - channel: String, - folder: String - ) { - self.channel = channel - self.folder = folder - } -} - -public struct RemoveWorkspaceFolderParams: Codable, Sendable { - /// Channel URI this command targets. - public var channel: String - /// Directory to revoke tool access to. - public var folder: String - - public init( - channel: String, - folder: String - ) { - self.channel = channel - self.folder = folder - } -} - -public struct WorkspaceFolderResult: Codable, Sendable { - /// The session's working directories after the mutation. - public var directories: [String] - - public init( - directories: [String] - ) { - self.directories = directories - } -} - -public struct AddWorkspaceFolderResult: Codable, Sendable { - /// The session's working directories after the mutation. - public var directories: [String] - - public init( - directories: [String] - ) { - self.directories = directories - } -} - -public struct RemoveWorkspaceFolderResult: Codable, Sendable { - /// The session's working directories after the mutation. - public var directories: [String] - - public init( - directories: [String] - ) { - self.directories = directories - } -} - public struct ChatForkSource: Codable, Sendable { /// URI of the existing chat to fork from public var chat: String @@ -548,69 +485,6 @@ public struct DisposeChatParams: Codable, Sendable { } } -public struct AddChatWorkspaceFolderParams: Codable, Sendable { - /// Channel URI this command targets. - public var channel: String - /// Directory to grant tool access to. Must be in the session's `workingDirectories`. - public var folder: String - - public init( - channel: String, - folder: String - ) { - self.channel = channel - self.folder = folder - } -} - -public struct RemoveChatWorkspaceFolderParams: Codable, Sendable { - /// Channel URI this command targets. - public var channel: String - /// Directory to revoke tool access to. - public var folder: String - - public init( - channel: String, - folder: String - ) { - self.channel = channel - self.folder = folder - } -} - -public struct ChatWorkspaceFolderResult: Codable, Sendable { - /// The chat's working directories after the mutation. - public var directories: [String] - - public init( - directories: [String] - ) { - self.directories = directories - } -} - -public struct AddChatWorkspaceFolderResult: Codable, Sendable { - /// The chat's working directories after the mutation. - public var directories: [String] - - public init( - directories: [String] - ) { - self.directories = directories - } -} - -public struct RemoveChatWorkspaceFolderResult: Codable, Sendable { - /// The chat's working directories after the mutation. - public var directories: [String] - - public init( - directories: [String] - ) { - self.directories = directories - } -} - public struct ListSessionsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift index 09b0608ea..0b39af3a7 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Messages.generated.swift @@ -88,14 +88,6 @@ public enum AHPCommands { JsonRpcRequest(id: id, method: "disposeSession", params: params) } - public static func addWorkspaceFolder(id: Int, params: AddWorkspaceFolderParams) -> JsonRpcRequest { - JsonRpcRequest(id: id, method: "addWorkspaceFolder", params: params) - } - - public static func removeWorkspaceFolder(id: Int, params: RemoveWorkspaceFolderParams) -> JsonRpcRequest { - JsonRpcRequest(id: id, method: "removeWorkspaceFolder", params: params) - } - public static func listSessions(id: Int, params: ListSessionsParams) -> JsonRpcRequest { JsonRpcRequest(id: id, method: "listSessions", params: params) } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 8f0843b03..f153c0299 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -182,19 +182,15 @@ public struct PartialSessionSummary: Codable, Sendable { public var activity: String? /// Server-owned project for this session public var project: ProjectInfo? - /// The default working directory URI for this session. Individual chats - /// MAY override via {@link ChatSummary.workingDirectory | their own - /// `workingDirectory`}; this field acts as the fallback for any chat that - /// does not. - public var workingDirectory: String? - /// The full set of working directories the session's agent has tool access - /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - /// entries are equal peers — there is no privileged "primary". Individual - /// chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - /// - /// When absent, fall back to {@link workingDirectory} (if set) as a - /// single-entry set. + /// The working directories the session's agent has tool access to, as + /// maintained by the `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` actions. Directories are equal peers + /// except when the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// entry is then a fixed process root). Individual chats MAY restrict to a + /// subset via {@link ChatSummary.workingDirectories | their own + /// `workingDirectories`}; a chat that sets none operates against this full + /// set. public var workingDirectories: [String]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render @@ -224,7 +220,6 @@ public struct PartialSessionSummary: Codable, Sendable { case status case activity case project - case workingDirectory case workingDirectories case annotations case resource @@ -240,7 +235,6 @@ public struct PartialSessionSummary: Codable, Sendable { status: SessionStatus? = nil, activity: String? = nil, project: ProjectInfo? = nil, - workingDirectory: String? = nil, workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, resource: String? = nil, @@ -254,7 +248,6 @@ public struct PartialSessionSummary: Codable, Sendable { self.status = status self.activity = activity self.project = project - self.workingDirectory = workingDirectory self.workingDirectories = workingDirectories self.annotations = annotations self.resource = resource diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 0cad1e666..f96402d01 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -616,9 +616,12 @@ public struct AgentCapabilities: Codable, Sendable { /// forking; set {@link MultipleChatsCapability.fork} to also allow forking. public var multipleChats: MultipleChatsCapability? /// The session's agent can be granted tool access to more than one working - /// directory, with all directories treated as equal peers (no primary). When - /// absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` - /// and MUST NOT set more than one entry in + /// directory. The directories are treated as equal peers except where the + /// agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + /// (some backends pin their first directory as a fixed process root). + /// + /// When absent, clients MUST NOT mutate a session's or chat's working-directory + /// set and MUST NOT set more than one entry in /// {@link CreateSessionParams.workingDirectories}. public var multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? @@ -645,10 +648,22 @@ public struct MultipleChatsCapability: Codable, Sendable { } public struct MultipleWorkspaceFoldersCapability: Codable, Sendable { + /// The agent's **first** working directory (index `0` of + /// {@link CreateSessionParams.workingDirectories}) is an immutable primary: + /// it is fixed for the lifetime of the session — clients MUST NOT remove or + /// reorder it. Additional directories after it remain equal peers that can be + /// added and removed freely. + /// + /// Advertised by backends whose agent process is rooted at a single directory + /// that cannot change once the session has started (e.g. the SDK's primary + /// `workingDirectory`). When absent or `false`, all directories are equal + /// peers and any of them may be removed. + public var immutablePrimary: Bool? public init( - + immutablePrimary: Bool? = nil ) { + self.immutablePrimary = immutablePrimary } } @@ -868,19 +883,17 @@ public struct ChatState: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? - /// Optional per-chat working directory. - public var workingDirectory: String? /// The subset of the session's /// {@link SessionState.workingDirectories | `workingDirectories`} that this /// chat's agent has tool access to. Every entry MUST be present in the owning - /// session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` - /// calls that violate this constraint. + /// session's `workingDirectories`; servers MUST reject a + /// `chat/workingDirectorySet` action that violates this constraint. /// /// When absent, the chat inherits the full session set. When present but empty /// (not recommended), the chat has no working-directory tool access at all. /// - /// Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the - /// set on a running chat. + /// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to + /// update the subset on a running chat. public var workingDirectories: [String]? /// Completed turns public var turns: [Turn] @@ -922,7 +935,6 @@ public struct ChatState: Codable, Sendable { case modifiedAt case origin case interactivity - case workingDirectory case workingDirectories case turns case turnsNextCursor @@ -942,7 +954,6 @@ public struct ChatState: Codable, Sendable { modifiedAt: String, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, - workingDirectory: String? = nil, workingDirectories: [String]? = nil, turns: [Turn], turnsNextCursor: String? = nil, @@ -960,7 +971,6 @@ public struct ChatState: Codable, Sendable { self.modifiedAt = modifiedAt self.origin = origin self.interactivity = interactivity - self.workingDirectory = workingDirectory self.workingDirectories = workingDirectories self.turns = turns self.turnsNextCursor = turnsNextCursor @@ -992,8 +1002,6 @@ public struct ChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? - /// Optional per-chat working directory. - public var workingDirectory: String? /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. public var workingDirectories: [String]? @@ -1006,7 +1014,6 @@ public struct ChatSummary: Codable, Sendable { modifiedAt: String, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, - workingDirectory: String? = nil, workingDirectories: [String]? = nil ) { self.resource = resource @@ -1016,7 +1023,6 @@ public struct ChatSummary: Codable, Sendable { self.modifiedAt = modifiedAt self.origin = origin self.interactivity = interactivity - self.workingDirectory = workingDirectory self.workingDirectories = workingDirectories } } @@ -1032,19 +1038,15 @@ public struct SessionState: Codable, Sendable { public var activity: String? /// Server-owned project for this session public var project: ProjectInfo? - /// The default working directory URI for this session. Individual chats - /// MAY override via {@link ChatSummary.workingDirectory | their own - /// `workingDirectory`}; this field acts as the fallback for any chat that - /// does not. - public var workingDirectory: String? - /// The full set of working directories the session's agent has tool access - /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - /// entries are equal peers — there is no privileged "primary". Individual - /// chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - /// - /// When absent, fall back to {@link workingDirectory} (if set) as a - /// single-entry set. + /// The working directories the session's agent has tool access to, as + /// maintained by the `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` actions. Directories are equal peers + /// except when the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// entry is then a fixed process root). Individual chats MAY restrict to a + /// subset via {@link ChatSummary.workingDirectories | their own + /// `workingDirectories`}; a chat that sets none operates against this full + /// set. public var workingDirectories: [String]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1121,7 +1123,7 @@ public struct SessionState: Codable, Sendable { /// /// Clients MAY look for well-known keys here to provide enhanced UI. /// For example, a `git` key may provide extra git metadata about the session's - /// workingDirectory. + /// working directories. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -1130,7 +1132,6 @@ public struct SessionState: Codable, Sendable { case status case activity case project - case workingDirectory case workingDirectories case annotations case lifecycle @@ -1152,7 +1153,6 @@ public struct SessionState: Codable, Sendable { status: SessionStatus, activity: String? = nil, project: ProjectInfo? = nil, - workingDirectory: String? = nil, workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, lifecycle: SessionLifecycle, @@ -1172,7 +1172,6 @@ public struct SessionState: Codable, Sendable { self.status = status self.activity = activity self.project = project - self.workingDirectory = workingDirectory self.workingDirectories = workingDirectories self.annotations = annotations self.lifecycle = lifecycle @@ -1327,19 +1326,15 @@ public struct SessionSummary: Codable, Sendable { public var activity: String? /// Server-owned project for this session public var project: ProjectInfo? - /// The default working directory URI for this session. Individual chats - /// MAY override via {@link ChatSummary.workingDirectory | their own - /// `workingDirectory`}; this field acts as the fallback for any chat that - /// does not. - public var workingDirectory: String? - /// The full set of working directories the session's agent has tool access - /// to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - /// entries are equal peers — there is no privileged "primary". Individual - /// chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - /// - /// When absent, fall back to {@link workingDirectory} (if set) as a - /// single-entry set. + /// The working directories the session's agent has tool access to, as + /// maintained by the `session/workingDirectorySet` / + /// `session/workingDirectoryRemoved` actions. Directories are equal peers + /// except when the agent advertises + /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// entry is then a fixed process root). Individual chats MAY restrict to a + /// subset via {@link ChatSummary.workingDirectories | their own + /// `workingDirectories`}; a chat that sets none operates against this full + /// set. public var workingDirectories: [String]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1369,7 +1364,6 @@ public struct SessionSummary: Codable, Sendable { case status case activity case project - case workingDirectory case workingDirectories case annotations case resource @@ -1385,7 +1379,6 @@ public struct SessionSummary: Codable, Sendable { status: SessionStatus, activity: String? = nil, project: ProjectInfo? = nil, - workingDirectory: String? = nil, workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, resource: String, @@ -1399,7 +1392,6 @@ public struct SessionSummary: Codable, Sendable { self.status = status self.activity = activity self.project = project - self.workingDirectory = workingDirectory self.workingDirectories = workingDirectories self.annotations = annotations self.resource = resource @@ -4520,15 +4512,24 @@ public struct Changeset: Codable, Sendable { /// Implementations MAY provide additional values; clients SHOULD fall back /// to a reasonable default when an unknown value is encountered. public var changeKind: String - /// The working directory this changeset is scoped to, when it covers changes - /// within a single directory of a multiroot session. MUST be one of the - /// session's {@link SessionState.workingDirectories}. + /// The working directory this changeset is scoped to. When set, it MUST be one + /// of the owning session's {@link SessionState.workingDirectories}, and every + /// file in the changeset belongs to that directory. + /// + /// **Grouping is the host's responsibility, not the client's.** A host whose + /// session has multiple working directories MUST group changes by directory — + /// emitting one changeset per working directory, each carrying its + /// `workingDirectory` — so that a client never has to derive a file's owning + /// directory itself (e.g. by prefix-matching URIs, which the client cannot do + /// correctly across nested repositories, symlinks, or submodules). This keeps + /// AHP a display-ready presentation model (see the + /// {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS + /// knowledge and hands clients pre-grouped changesets. /// - /// A host with multiple working directories groups changes by directory by - /// advertising one changeset per directory, each carrying its - /// `workingDirectory` — rather than nesting changes as arrays-of-arrays. - /// Omit for changesets that are not directory-scoped (e.g. a single-directory - /// session, or a session-wide roll-up spanning every directory). + /// Omit only for changesets that are genuinely not scoped to a single working + /// directory — e.g. a single-directory session (nothing to group), or an + /// aggregate roll-up / out-of-tree changeset that intentionally spans (or sits + /// outside) the working directories. public var workingDirectory: String? /// Optional capability declarations for this changeset. Absent (or an empty /// object) means the changeset advertises no optional capabilities. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 58c67fc39..a84f8708c 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -827,7 +827,7 @@ private func mergeChatSummaryChanges(_ summary: inout ChatSummary, changes: Part if let activity = changes.activity { summary.activity = activity } if let modifiedAt = changes.modifiedAt { summary.modifiedAt = modifiedAt } if let origin = changes.origin { summary.origin = origin } - if let workingDirectory = changes.workingDirectory { summary.workingDirectory = workingDirectory } + if let workingDirectories = changes.workingDirectories { summary.workingDirectories = workingDirectories } } private func chatSummaryStatus(_ state: ChatState, terminalStatus: SessionStatus? = nil) -> SessionStatus { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift index a9bb5c82c..6bb1333fc 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift @@ -829,6 +829,6 @@ private func applySummaryChanges( if let v = changes.modifiedAt { existing.modifiedAt = v } if let v = changes.project { existing.project = v } if let v = changes.annotations { existing.annotations = v } - if let v = changes.workingDirectory { existing.workingDirectory = v } + if let v = changes.workingDirectories { existing.workingDirectories = v } } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index 39cadb400..eaca252b7 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -17,10 +17,6 @@ the tag matches the version pinned in [`VERSION`](VERSION). ## [Unreleased] -### Added - -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. - ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 1ef6d4384..d17e3cf93 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -20,10 +20,6 @@ hotfix escape hatch. ## [Unreleased] -### Added - -- `multipleWorkspaceFolders` capability on `AgentCapabilities`, `CreateSessionParams.workingDirectories`, and `addWorkspaceFolder` / `removeWorkspaceFolder` (session-level) and `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` (chat-level subset) commands, plus `workingDirectories` fields on `SessionMetadata`, `ChatState`, and `ChatSummary` for multiroot session support (AHP 0.6.0). Changes are grouped by directory via `Changeset.workingDirectory`. - ## [0.5.2] — 2026-07-09 Implements AHP 0.5.2. diff --git a/clients/typescript/src/client/hosts/runtime.ts b/clients/typescript/src/client/hosts/runtime.ts index f5e2c0150..09e3ae830 100644 --- a/clients/typescript/src/client/hosts/runtime.ts +++ b/clients/typescript/src/client/hosts/runtime.ts @@ -879,7 +879,7 @@ function applySummaryChange( if (changes.activity !== undefined) merged.activity = changes.activity; if (changes.modifiedAt !== undefined) merged.modifiedAt = changes.modifiedAt; if (changes.project !== undefined) merged.project = changes.project; - if (changes.workingDirectory !== undefined) merged.workingDirectory = changes.workingDirectory; + if (changes.workingDirectories !== undefined) merged.workingDirectories = changes.workingDirectories; if (changes._meta !== undefined) merged._meta = changes._meta; cache.set(params.session, merged); } diff --git a/docs/.changes/20260716-multiroot-sessions.json b/docs/.changes/20260716-multiroot-sessions.json new file mode 100644 index 000000000..6ef5062ae --- /dev/null +++ b/docs/.changes/20260716-multiroot-sessions.json @@ -0,0 +1,5 @@ +{ + "type": "added", + "message": "Multiroot session support: `AgentCapabilities.multipleWorkspaceFolders` capability (with `immutablePrimary`), `CreateSessionParams.workingDirectories` and `SessionMetadata.workingDirectories` (equal-peer set, mirrored onto `SessionState`/`SessionSummary`), `CreateChatParams.workingDirectories` and `ChatState`/`ChatSummary.workingDirectories` (a subset of the session's set), the client-dispatchable `session/workingDirectorySet` / `session/workingDirectoryRemoved` and `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions for mutating those sets, and `Changeset.workingDirectory` (with a `'directory'` `changeKind`) so a multiroot host groups changes by directory.", + "issues": [337] +} diff --git a/docs/guide/changesets.md b/docs/guide/changesets.md index c2d79cac8..05809bf2f 100644 --- a/docs/guide/changesets.md +++ b/docs/guide/changesets.md @@ -64,7 +64,11 @@ unknown variables. ### Multiroot Sessions -A session with multiple [working directories](/guide/state-model#multiroot-sessions) groups its changes **by directory**: the host advertises one catalogue entry per working directory, each carrying a `workingDirectory` (one of the session's `workingDirectories`) and typically `changeKind: 'directory'`. This keeps each changeset a flat list of files rather than nesting changes as arrays-of-arrays. A host MAY additionally advertise a session-wide entry (no `workingDirectory`) that rolls up every directory. +Grouping changes by directory is the **host's** responsibility, not the client's. A host whose session has multiple [working directories](/guide/state-model#multiroot-sessions) **MUST** group its changes by directory — advertising one catalogue entry per working directory, each carrying a `workingDirectory` (one of the session's `workingDirectories`) and typically `changeKind: 'directory'`. Each such changeset is a flat list of files belonging to that one directory. + +This follows AHP's [doctrine](/guide/doctrine): the protocol is a display-ready presentation model in which the host owns the authoritative filesystem/VCS knowledge. A client MUST NOT be required to derive a file's owning directory itself (e.g. by prefix-matching URIs) — which it cannot do correctly across nested repositories, symlinks, or submodules. The host, which knows the real repository boundaries, hands clients pre-grouped changesets. + +`workingDirectory` is omitted only for changesets that are genuinely not scoped to one directory: a single-directory session, or an intentional aggregate roll-up / out-of-tree changeset. ### Changeset State diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index de5c5461e..c02da1f5f 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -73,7 +73,6 @@ SessionState { status: number // SessionStatus bitset activity?: string project?: ProjectInfo - workingDirectory?: URI // deprecated: use workingDirectories workingDirectories?: URI[] // equal-peer working directories annotations?: AnnotationsSummary @@ -108,7 +107,6 @@ SessionSummary { createdAt: string // ISO 8601, e.g. "2025-03-10T18:42:03.123Z" modifiedAt: string // ISO 8601 project?: ProjectInfo - workingDirectory?: URI // deprecated: use workingDirectories workingDirectories?: URI[] // equal-peer working directories annotations?: AnnotationsSummary changes?: ChangesSummary @@ -152,7 +150,6 @@ ChatState { activity?: string modifiedAt: string origin?: ChatOrigin // how the chat came to exist (user / fork / tool) - workingDirectory?: URI // deprecated: use workingDirectories workingDirectories?: URI[] // subset of session's workingDirectories turns: Turn[] // completed turns @@ -579,27 +576,29 @@ A client MUST NOT pass more than one entry unless the agent advertises `multipleWorkspaceFolders`. Servers without that capability treat only the first entry as the session's working directory and ignore the rest. -The singular `workingDirectory` field is deprecated in favour of -`workingDirectories` but is retained as a backwards-compatible shorthand; -servers MUST still honour it when `workingDirectories` is absent. +The directories are equal peers unless the agent advertises +`multipleWorkspaceFolders.immutablePrimary`, in which case the first entry is a +fixed process root that clients MUST NOT remove or reorder. Forked sessions ignore `workingDirectories` — they inherit the working directories of the source session. ### Managing directories after creation -Two commands let clients mutate the directory set on a running session: +The directory set is state (`SessionState.workingDirectories`), so clients +mutate it by **dispatching actions**, not by calling commands: -| Command | Effect | +| Action | Effect | | --- | --- | -| `addWorkspaceFolder` | Grants tool access to the given directory. Adding a directory already in the set is a no-op. | -| `removeWorkspaceFolder` | Revokes tool access to the given directory. There is no atomic server-side "remove one" primitive; the server reconfigures to the reduced set. Removing a directory not in the set is a no-op. A server MAY return an error if it cannot relinquish a directory while the session is live. | +| `session/workingDirectorySet` | Adds `directory` to the set (creating it if absent). A no-op when the directory is already present. | +| `session/workingDirectoryRemoved` | Removes `directory` from the set. A no-op when it is not present. There is no atomic backend "remove one" primitive — the host reconfigures its agent to the reduced set. A host MAY decline to apply the removal (e.g. an immutable primary), leaving the set unchanged. | -Both commands return the **full directory set after the mutation** (the -`directories` field of `AddWorkspaceFolderResult` / `RemoveWorkspaceFolderResult`). +Both are `@clientDispatchable`. The resulting set is observed on +`SessionState.workingDirectories` like any other state — there is no separate +result payload. -Before issuing either command, a client MUST verify that the agent advertises -`multipleWorkspaceFolders`; servers MUST reject these commands otherwise. +Before dispatching either action, a client MUST verify that the agent advertises +`multipleWorkspaceFolders`. ### Per-chat working-directory subsets @@ -630,18 +629,17 @@ Forked chats (those with a `source`) inherit the source chat's #### Managing the subset after creation -Two commands mutate a running chat's working-directory subset: +Two `@clientDispatchable` actions mutate a running chat's working-directory +subset: -| Command | Effect | +| Action | Effect | | --- | --- | -| `addChatWorkspaceFolder` | Adds a directory to the chat's subset. The directory MUST already be in the session's `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. Adding a directory already in the chat's set is a no-op. | -| `removeChatWorkspaceFolder` | Removes a directory from the chat's subset (idempotent). | +| `chat/workingDirectorySet` | Adds `directory` to the chat's subset. It MUST already be in the session's `workingDirectories`; a host MUST reject a directory that is not. A no-op when already in the chat's subset. | +| `chat/workingDirectoryRemoved` | Removes `directory` from the chat's subset (idempotent). Only affects the chat — the directory stays in the session's set. | -Both commands return the **full chat subset after the mutation** (the -`directories` field of `AddChatWorkspaceFolderResult` / -`RemoveChatWorkspaceFolderResult`). +The subset is observed on `ChatState.workingDirectories`. -A client MUST NOT issue these commands unless the agent advertises +A client MUST NOT dispatch these actions unless the agent advertises `multipleWorkspaceFolders`. ## Next Steps diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index 728f4fabc..ab6b58ecd 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -127,14 +127,14 @@ capabilities: working directory, each tagged with the directory it covers — so a UI can show "changes in repo-a / repo-b" without inventing arrays-of-arrays. -6. **Graceful backward compatibility.** The old singular `workingDirectory` - fields are kept as a deprecated single-directory shorthand, so clients that - predate this feature keep working unchanged. +6. **Server-owned change grouping.** A multiroot host MUST group changes by + directory — one changeset per working directory, each tagged with it — so + clients never re-derive a file's owning directory themselves. ```mermaid flowchart LR Cap["capability:
multipleWorkspaceFolders?"] --> Create["createSession
workingDirectories[]"] - Create --> Add["addWorkspaceFolder /
removeWorkspaceFolder"] + Create --> Add["session/workingDirectorySet /
Removed actions"] Create --> Chat["chat subset
workingDirectories ⊆ session"] Create --> CS["changesets
grouped by workingDirectory"] ``` @@ -151,9 +151,9 @@ kept as two separate checkouts. | User starts a task over `api/` and `client/` | A session with `workingDirectories: [api, client]`. | | Agent edits both repos | The agent has tool access to both directories, equal peers. | | User opens a focused thread on just the client | A chat pinned to `workingDirectories: [client]`. | -| Agent later needs the shared `protos/` repo too | `addWorkspaceFolder(protos)` → set becomes `[api, client, protos]`. | +| Agent later needs the shared `protos/` repo too | dispatch `session/workingDirectorySet(protos)` → set becomes `[api, client, protos]`. | | User reviews the diff | Three changesets, one per directory, each tagged with its `workingDirectory`. | -| The `protos/` work turns out unnecessary | `removeWorkspaceFolder(protos)` → set reconfigures back to `[api, client]`. | +| The `protos/` work turns out unnecessary | dispatch `session/workingDirectoryRemoved(protos)` → set reconfigures back to `[api, client]`. | The session stays one coherent conversation and one shared configuration throughout — no juggling three disconnected sessions. @@ -169,9 +169,10 @@ throughout — no juggling three disconnected sessions. - **It is not per-file or per-glob scoping.** The unit is a working directory (a root), not arbitrary path patterns within it. -- **It does not introduce a primary.** All directories are peers. Anything that - needs "the main one" (e.g. an old client) reads the deprecated singular field, - which is just a shorthand — not a semantic primary. +- **No mandatory primary.** All directories are peers by default. A backend that + *must* pin its first directory (a fixed process root) advertises + `immutablePrimary` on the capability; clients then keep index `0` fixed. The + protocol has no other notion of a "main" directory. - **It is not multi-root *chats as sub-sessions*.** A chat narrowing to a subset is still one thread under one session's trust and identity. Independent agents @@ -228,20 +229,23 @@ as today. | Symbol | File | Kind | | --- | --- | --- | | `AgentCapabilities.multipleWorkspaceFolders?` | `channels-root/state.ts` | added (capability) | -| `MultipleWorkspaceFoldersCapability` | `channels-root/state.ts` | added (type) | +| `MultipleWorkspaceFoldersCapability` (`immutablePrimary?`) | `channels-root/state.ts` | added (type) | | `CreateSessionParams.workingDirectories?` | `channels-session/commands.ts` | added | -| `CreateSessionParams.workingDirectory?` | `channels-session/commands.ts` | **deprecated** | | `SessionMetadata.workingDirectories?` (→ `SessionState`, `SessionSummary`) | `channels-session/state.ts` | added | -| `SessionMetadata.workingDirectory?` | `channels-session/state.ts` | **deprecated** | -| `addWorkspaceFolder` / `removeWorkspaceFolder` commands | `channels-session/commands.ts` | added (command) | -| `WorkspaceFolderResult` (+ `Add…`/`Remove…Result`) | `channels-session/commands.ts` | added (type) | +| `session/workingDirectorySet` / `session/workingDirectoryRemoved` actions | `channels-session/actions.ts` | added (action) | | `ChatState.workingDirectories?` / `ChatSummary.workingDirectories?` | `channels-chat/state.ts` | added | -| `ChatState.workingDirectory?` / `ChatSummary.workingDirectory?` | `channels-chat/state.ts` | **deprecated** | | `CreateChatParams.workingDirectories?` | `channels-chat/commands.ts` | added | -| `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` commands | `channels-chat/commands.ts` | added (command) | -| `ChatWorkspaceFolderResult` (+ `Add…`/`Remove…Result`) | `channels-chat/commands.ts` | added (type) | +| `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions | `channels-chat/actions.ts` | added (action) | | `Changeset.workingDirectory?` + `'directory'` `changeKind` | `channels-changeset/state.ts` | added | -| 4 command entries in `CommandMap` | `common/messages.ts` | added | +| 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.6.0`) | `common/actions.ts`, `version/registry.ts` | added | + +> **Revised after review.** The directory-mutation surface started as four +> **commands** (`add/removeWorkspaceFolder`, `add/removeChatWorkspaceFolder` +> with request/result types). Per review, it is now four **state actions** +> following the keyed-collection convention — the set lives in state, so clients +> mutate it by dispatching actions and observe the result on +> `workingDirectories`. The deprecated singular `workingDirectory` fields were +> **hard-removed** (0.6.0 is a breaking release), not kept as a shorthand. ### 8.2 Type signatures @@ -252,108 +256,120 @@ interface AgentCapabilities { /** Presence ({}) = the agent supports >1 working directory per session. */ multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability; } -interface MultipleWorkspaceFoldersCapability {} // presence-flag, reserved for options +interface MultipleWorkspaceFoldersCapability { + /** First directory is a fixed process root; clients MUST NOT remove/reorder it. */ + immutablePrimary?: boolean; +} // ── Session create — channels-session/commands.ts ──────────────────────── interface CreateSessionParams extends BaseParams { // …existing… - /** The session's equal-peer working directories (no "primary"). */ + /** The session's equal-peer working directories. */ workingDirectories?: URI[]; - /** @deprecated single-directory shorthand; use workingDirectories. */ - workingDirectory?: URI; } // ── Session state — channels-session/state.ts (→ SessionState/SessionSummary) interface SessionMetadata { // …existing… workingDirectories?: URI[]; - /** @deprecated mirror of workingDirectories[0] for old clients. */ - workingDirectory?: URI; } -// ── Session runtime mutation — channels-session/commands.ts ─────────────── -// channel = session URI. Gated by multipleWorkspaceFolders. -interface AddWorkspaceFolderParams extends BaseParams { folder: URI; } -interface RemoveWorkspaceFolderParams extends BaseParams { folder: URI; } -interface WorkspaceFolderResult { directories: URI[]; } // full set after mutation -interface AddWorkspaceFolderResult extends WorkspaceFolderResult {} -interface RemoveWorkspaceFolderResult extends WorkspaceFolderResult {} +// ── Session runtime mutation — channels-session/actions.ts ──────────────── +// channel = session URI. Gated by multipleWorkspaceFolders. @clientDispatchable. +interface SessionWorkingDirectorySetAction { + type: ActionType.SessionWorkingDirectorySet; // 'session/workingDirectorySet' + directory: URI; // appended; no-op if present +} +interface SessionWorkingDirectoryRemovedAction { + type: ActionType.SessionWorkingDirectoryRemoved; // 'session/workingDirectoryRemoved' + directory: URI; // removed; no-op if absent +} -// ── Chat — channels-chat/state.ts & commands.ts ────────────────────────── +// ── Chat — channels-chat/state.ts, commands.ts & actions.ts ────────────── interface ChatState /* and ChatSummary */ { // …existing… /** The chat's subset — every entry MUST be one of the session's dirs. */ workingDirectories?: URI[]; - /** @deprecated single-directory shorthand. */ - workingDirectory?: URI; } interface CreateChatParams extends BaseParams { // …existing… workingDirectories?: URI[]; // subset ⊆ session; absent → whole session set } -// channel = chat URI. Gated by multipleWorkspaceFolders. -interface AddChatWorkspaceFolderParams extends BaseParams { folder: URI; } -interface RemoveChatWorkspaceFolderParams extends BaseParams { folder: URI; } -interface ChatWorkspaceFolderResult { directories: URI[]; } -interface AddChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} -interface RemoveChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} +// channel = chat URI. Gated by multipleWorkspaceFolders. @clientDispatchable. +interface ChatWorkingDirectorySetAction { + type: ActionType.ChatWorkingDirectorySet; // 'chat/workingDirectorySet' + directory: URI; // MUST be in the session set +} +interface ChatWorkingDirectoryRemovedAction { + type: ActionType.ChatWorkingDirectoryRemoved; // 'chat/workingDirectoryRemoved' + directory: URI; +} // ── Changes — channels-changeset/state.ts ──────────────────────────────── interface Changeset { // …existing… changeKind: string; // now also accepts 'directory' - /** Directory this changeset is scoped to (one of the session's dirs). */ + /** + * Directory this changeset is scoped to (one of the session's dirs). + * A multiroot host MUST group changes by directory (one changeset per dir); + * omitted only for single-dir sessions or out-of-tree/aggregate changesets. + */ workingDirectory?: URI; } - -// ── Command registry — common/messages.ts ──────────────────────────────── -interface CommandMap { - // …existing… - addWorkspaceFolder: { params: AddWorkspaceFolderParams; result: AddWorkspaceFolderResult }; - removeWorkspaceFolder: { params: RemoveWorkspaceFolderParams; result: RemoveWorkspaceFolderResult }; - addChatWorkspaceFolder: { params: AddChatWorkspaceFolderParams; result: AddChatWorkspaceFolderResult }; - removeChatWorkspaceFolder: { params: RemoveChatWorkspaceFolderParams; result: RemoveChatWorkspaceFolderResult }; -} ``` ### 8.3 Versioning & gating -- `PROTOCOL_VERSION` is **`0.6.0`** (a capability boundary; the branch's base +- `PROTOCOL_VERSION` is **`0.6.0`** (a breaking release; the branch's base already advanced the ongoing-dev version to `0.6.0`). `SUPPORTED_PROTOCOL_VERSIONS` = `[0.6.0, 0.5.2, 0.5.1]`. -- The four new commands are gated by the `multipleWorkspaceFolders` capability - **plus** the `initialize` version handshake — not by the - `ACTION_INTRODUCED_IN` / `NOTIFICATION_INTRODUCED_IN` maps (those track state - actions and server notifications; commands are gated like `createChat`). -- Removal (`removeWorkspaceFolder` / `removeChatWorkspaceFolder`) is idempotent - and modelled as *reconfigure-to-the-reduced-set*; a server MAY refuse a - directory it cannot relinquish while live. +- The four directory mutations are **state actions**, so they carry + `ACTION_INTRODUCED_IN` entries at `0.6.0` (and are `@clientDispatchable`). + Everything else — the capability, the create-time fields, and the + `Changeset.workingDirectory` field — is gated by the `multipleWorkspaceFolders` + capability plus the `initialize` version handshake. +- Removal actions are idempotent and modelled as + *reconfigure-to-the-reduced-set*; a host MAY decline to apply a removal (e.g. + an `immutablePrimary` directory), leaving the set unchanged. --- ## 9. Design decisions & resolved questions -- **No primary directory.** *Resolved:* the set is equal peers. Rejected a +- **No mandatory primary.** *Resolved:* the set is equal peers. Rejected a "primary + additional" split because it re-introduces the confusion the user - called out ("what's the relation between primary and secondary?"). - -- **Deprecate, don't remove, `workingDirectory`.** *Resolved:* the singular field - is read by every existing client from session state; removing it would break - single-directory sessions too. It stays as a deprecated shorthand. + called out ("what's the relation between primary and secondary?"). Backends + that genuinely pin a fixed process root opt in via + `MultipleWorkspaceFoldersCapability.immutablePrimary` (index `0` fixed). + +- **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* 0.6.0 + is a breaking release, so the deprecated singular fields on + `CreateSessionParams` / `SessionMetadata` / `ChatState` / `ChatSummary` are + removed outright rather than kept as a shorthand. + +- **Directory mutations are state actions, not commands.** *Resolved (per + review):* `workingDirectories` is a keyed collection, so it follows the + established `*/workingDirectorySet` + `*/workingDirectoryRemoved` action + convention (session and chat) with pure reducers, rather than + request/response commands. The set lives in state; clients observe the result + there. - **Chat narrows to a subset (not exactly one).** *Resolved:* an earlier draft made a chat operate in exactly one directory; this was widened to "a subset ⊆ the session's set," with absent meaning the whole set. Gated by the same capability. -- **Changes grouped by directory, on the changeset.** *Resolved:* put a - `workingDirectory` on `Changeset` and advertise one per directory, rather than - an array-of-arrays or a per-file `root` tag. Reuses the catalogue abstraction - and keeps each changeset flat. +- **Changes grouped by directory — server's job.** *Resolved (per review + pushback):* a multiroot host MUST group changes by directory + (`Changeset.workingDirectory`, one changeset per dir). Grouping is a host + responsibility per AHP's doctrine (display-ready state; host owns the + filesystem/VCS model) — clients must not re-derive it from file URIs, which + they cannot do correctly across nested repos/symlinks. The field stays + optional only for single-dir sessions and out-of-tree/aggregate changesets. -- **Removal semantics.** *Resolved:* no single-remove primitive is assumed; - `removeWorkspaceFolder` reconfigures to the reduced set and returns it, so it is - idempotent and safe to retry. +- **Removal semantics.** *Resolved:* no single-remove primitive is assumed; the + `*/workingDirectoryRemoved` action reduces to the reduced set, so it is + idempotent and safe to retry. A host MAY decline (e.g. an immutable primary). --- @@ -365,11 +381,6 @@ interface CommandMap { Whether — and how — to make config resolution multiroot-aware is left as a follow-up. -- **Hard removal of the singular field.** Since the spec is pre-1.0, a future - release *could* drop `workingDirectory` entirely. Kept deprecated for now to - avoid breaking existing single-directory clients; promote to removal later if - desired. - - **Per-directory rollups on the lightweight summary.** `ChangesSummary` is a single aggregate today. If session-list UIs want per-repo badges without subscribing, a `byDirectory` rollup could be added later — deferred to keep the @@ -384,10 +395,10 @@ interface CommandMap { - **Before:** a session is scoped to one `workingDirectory`. - **After:** a session owns a **set of equal-peer directories**; a **chat** works - in a **subset**; **changes group by directory**. + in a **subset**; **changes group by directory** (server-side). - **Why:** cross-directory / multi-repo / multi-root-workspace tasks are one coherent piece of work, not N disconnected sessions. -- **How it stays safe:** capability-gated, additive, singular field deprecated - (not removed), removal modelled as reconfigure-to-reduced-set. -- **What it is not:** not a permission model, not per-file scoping, not a primary - directory, not sub-sessions — those are separate/future axes. +- **How it stays safe:** capability-gated, additive fields; directory mutations + are idempotent client-dispatchable actions; removal is reconfigure-to-reduced-set. +- **What it is not:** not a permission model, not per-file scoping, not a + mandatory primary, not sub-sessions — those are separate/future axes. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index f14c83551..bd1286283 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -233,10 +233,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -412,6 +408,40 @@ "clientId" ] }, + "SessionWorkingDirectorySetAction": { + "type": "object", + "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkspaceFolders}.", + "properties": { + "type": { + "const": "session/workingDirectorySet" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to grant the session's agent tool access to." + } + }, + "required": [ + "type", + "directory" + ] + }, + "SessionWorkingDirectoryRemovedAction": { + "type": "object", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "properties": { + "type": { + "const": "session/workingDirectoryRemoved" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to revoke the session's agent tool access to." + } + }, + "required": [ + "type", + "directory" + ] + }, "SessionInputNeededSetAction": { "type": "object", "description": "A session-level input request was added or updated.\n\nUpsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the\nhost dispatches this with the full {@link SessionInputRequest} to append a new\nentry to {@link SessionState.inputNeeded} or replace the existing entry with\nthe same `id`.\n\nServer-originated: the host mirrors chat-level requests (elicitations, tool\nconfirmations, client-tool executions) into the session aggregate so clients\nsubscribed only to the session channel can discover them. Clients respond by\ndispatching the ordinary `chat/*` action to the entry's `chat` channel — see\n{@link SessionInputRequest}.", @@ -1192,6 +1222,40 @@ "type" ] }, + "ChatWorkingDirectorySetAction": { + "type": "object", + "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}.", + "properties": { + "type": { + "const": "chat/workingDirectorySet" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to add to this chat's subset." + } + }, + "required": [ + "type", + "directory" + ] + }, + "ChatWorkingDirectoryRemovedAction": { + "type": "object", + "description": "A working directory was removed from this chat's\n{@link ChatState.workingDirectories} subset.\n\nRemoves `directory` from the chat's subset; a no-op when it is not present.\nIdempotent, mirroring `session/workingDirectoryRemoved`. Only affects the\nchat's subset — the directory remains in the session's set.", + "properties": { + "type": { + "const": "chat/workingDirectoryRemoved" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to remove from this chat's subset." + } + }, + "required": [ + "type", + "directory" + ] + }, "ChatUsageAction": { "type": "object", "description": "Token usage report for a turn.", @@ -2007,6 +2071,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatWorkingDirectorySetAction" + }, + { + "$ref": "#/$defs/ChatWorkingDirectoryRemovedAction" + }, { "$ref": "#/$defs/ChatUsageAction" }, @@ -2578,7 +2648,7 @@ }, "multipleWorkspaceFolders": { "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -2594,8 +2664,13 @@ }, "MultipleWorkspaceFoldersCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", - "properties": {} + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "properties": { + "immutablePrimary": { + "type": "boolean", + "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + } + } }, "SessionModelInfo": { "type": "object", @@ -2711,16 +2786,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2757,16 +2828,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2833,7 +2900,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworkingDirectory." + "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworking directories." } }, "required": [ @@ -3011,7 +3078,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectories`: the session-level set. Individual chats MAY restrict\n to a subset via {@link ChatSummary.workingDirectories}; aggregating these\n up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -3033,16 +3100,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -4268,16 +4331,12 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, "turns": { "type": "array", @@ -4362,10 +4421,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -6334,7 +6389,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." + "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", @@ -7083,6 +7138,12 @@ { "$ref": "#/$defs/SessionActiveClientRemovedAction" }, + { + "$ref": "#/$defs/SessionWorkingDirectorySetAction" + }, + { + "$ref": "#/$defs/SessionWorkingDirectoryRemovedAction" + }, { "$ref": "#/$defs/SessionInputNeededSetAction" }, @@ -7170,6 +7231,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatWorkingDirectorySetAction" + }, + { + "$ref": "#/$defs/ChatWorkingDirectoryRemovedAction" + }, { "$ref": "#/$defs/ChatUsageAction" }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index fb8191c82..86ca15fa0 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -960,10 +960,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories, all of which are equal peers —\nthere is no privileged \"primary\" directory.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder`\nto change the set after the session has started.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." - }, - "workingDirectory": { - "$ref": "#/$defs/URI" + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -1000,90 +997,6 @@ "channel" ] }, - "AddWorkspaceFolderParams": { - "type": "object", - "description": "Grants the session's agent tool access to a working directory, adding it to\nthe session's set of {@link CreateSessionParams.workingDirectories | working\ndirectories}. Only valid when the agent advertises\n`multipleWorkspaceFolders`; servers MUST reject this command otherwise.\n\nAll directories in the set are equal peers — there is no privileged\n\"primary\". Adding a directory that is already in the set is a no-op that\nstill returns the current full set.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to grant tool access to." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "RemoveWorkspaceFolderParams": { - "type": "object", - "description": "Revokes the session's agent tool access to one of its working directories.\nThere is no server-side primitive to \"remove\" a single directory mid-session\n— the server instead reconfigures the agent with the reduced directory set\nand returns it, so this command is safe to model as idempotent: removing a\ndirectory that is not in the set is a no-op that still returns the current\nfull set.\n\nA server MAY refuse to remove a directory it cannot relinquish while the\nsession is live (for example one bound to the running agent process),\nreturning an error rather than a result. The protocol itself designates no\ndirectory as special.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to revoke tool access to." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "WorkspaceFolderResult": { - "type": "object", - "description": "Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder`\ncommands: the session's full set of working directories after the mutation.\nAll entries are equal peers; the order carries no meaning.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The session's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "AddWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The session's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "RemoveWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The session's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, "FetchTurnsParams": { "type": "object", "description": "Requests that the host load older historical turns into a chat state.\n\nThe command result does not carry turns. Instead, before responding, the host\nMUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat\nchannel's `turns` state, ahead of the already-loaded window, and update or\nclear `turnsNextCursor`.\n\nBefore applying any operation that references a turn outside the currently\nloaded window, the host MUST eagerly load enough older turns into state for\nthat operation to reduce against valid state.", @@ -1240,90 +1153,6 @@ "channel" ] }, - "AddChatWorkspaceFolderParams": { - "type": "object", - "description": "Grants this chat's agent tool access to a working directory, adding it to\nthe chat's {@link ChatState.workingDirectories | `workingDirectories`} subset.\nThe directory MUST already be present in the owning session's\n`workingDirectories`; servers MUST reject with `InvalidParams` otherwise.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`. Adding a\ndirectory already in the chat's set is a no-op that still returns the current\nfull set.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to grant tool access to. Must be in the session's `workingDirectories`." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "RemoveChatWorkspaceFolderParams": { - "type": "object", - "description": "Revokes this chat's agent tool access to one of its working directories.\nAnalogous to the session-level `removeWorkspaceFolder`: the server\nreconfigures the chat to the reduced subset and returns it. Removing a\ndirectory not in the chat's set is a no-op that still returns the current\nfull set.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to revoke tool access to." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "ChatWorkspaceFolderResult": { - "type": "object", - "description": "Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`:\nthe chat's full working-directory subset after the mutation.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The chat's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "AddChatWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The chat's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "RemoveChatWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The chat's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, "CreateTerminalParams": { "type": "object", "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", @@ -2020,7 +1849,7 @@ }, "multipleWorkspaceFolders": { "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -2036,8 +1865,13 @@ }, "MultipleWorkspaceFoldersCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", - "properties": {} + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "properties": { + "immutablePrimary": { + "type": "boolean", + "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + } + } }, "SessionModelInfo": { "type": "object", @@ -2153,16 +1987,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2199,16 +2029,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2275,7 +2101,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworkingDirectory." + "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworking directories." } }, "required": [ @@ -2453,7 +2279,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectories`: the session-level set. Individual chats MAY restrict\n to a subset via {@link ChatSummary.workingDirectories}; aggregating these\n up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -2475,16 +2301,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3710,16 +3532,12 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, "turns": { "type": "array", @@ -3804,10 +3622,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -5776,7 +5590,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." + "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", @@ -6323,10 +6137,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -6502,6 +6312,40 @@ "clientId" ] }, + "SessionWorkingDirectorySetAction": { + "type": "object", + "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkspaceFolders}.", + "properties": { + "type": { + "const": "session/workingDirectorySet" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to grant the session's agent tool access to." + } + }, + "required": [ + "type", + "directory" + ] + }, + "SessionWorkingDirectoryRemovedAction": { + "type": "object", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "properties": { + "type": { + "const": "session/workingDirectoryRemoved" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to revoke the session's agent tool access to." + } + }, + "required": [ + "type", + "directory" + ] + }, "SessionInputNeededSetAction": { "type": "object", "description": "A session-level input request was added or updated.\n\nUpsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the\nhost dispatches this with the full {@link SessionInputRequest} to append a new\nentry to {@link SessionState.inputNeeded} or replace the existing entry with\nthe same `id`.\n\nServer-originated: the host mirrors chat-level requests (elicitations, tool\nconfirmations, client-tool executions) into the session aggregate so clients\nsubscribed only to the session channel can discover them. Clients respond by\ndispatching the ordinary `chat/*` action to the entry's `chat` channel — see\n{@link SessionInputRequest}.", @@ -7282,6 +7126,40 @@ "type" ] }, + "ChatWorkingDirectorySetAction": { + "type": "object", + "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}.", + "properties": { + "type": { + "const": "chat/workingDirectorySet" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to add to this chat's subset." + } + }, + "required": [ + "type", + "directory" + ] + }, + "ChatWorkingDirectoryRemovedAction": { + "type": "object", + "description": "A working directory was removed from this chat's\n{@link ChatState.workingDirectories} subset.\n\nRemoves `directory` from the chat's subset; a no-op when it is not present.\nIdempotent, mirroring `session/workingDirectoryRemoved`. Only affects the\nchat's subset — the directory remains in the session's set.", + "properties": { + "type": { + "const": "chat/workingDirectoryRemoved" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to remove from this chat's subset." + } + }, + "required": [ + "type", + "directory" + ] + }, "ChatUsageAction": { "type": "object", "description": "Token usage report for a turn.", @@ -8090,6 +7968,12 @@ { "$ref": "#/$defs/SessionActiveClientRemovedAction" }, + { + "$ref": "#/$defs/SessionWorkingDirectorySetAction" + }, + { + "$ref": "#/$defs/SessionWorkingDirectoryRemovedAction" + }, { "$ref": "#/$defs/SessionInputNeededSetAction" }, @@ -8177,6 +8061,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatWorkingDirectorySetAction" + }, + { + "$ref": "#/$defs/ChatWorkingDirectoryRemovedAction" + }, { "$ref": "#/$defs/ChatUsageAction" }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 5b07bd0b3..c9639921a 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -630,7 +630,7 @@ }, "multipleWorkspaceFolders": { "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -646,8 +646,13 @@ }, "MultipleWorkspaceFoldersCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", - "properties": {} + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "properties": { + "immutablePrimary": { + "type": "boolean", + "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + } + } }, "SessionModelInfo": { "type": "object", @@ -763,16 +768,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -809,16 +810,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -885,7 +882,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworkingDirectory." + "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworking directories." } }, "required": [ @@ -1063,7 +1060,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectories`: the session-level set. Individual chats MAY restrict\n to a subset via {@link ChatSummary.workingDirectories}; aggregating these\n up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -1085,16 +1082,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2320,16 +2313,12 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, "turns": { "type": "array", @@ -2414,10 +2403,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -4386,7 +4371,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." + "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", @@ -5660,10 +5645,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories, all of which are equal peers —\nthere is no privileged \"primary\" directory.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder`\nto change the set after the session has started.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." - }, - "workingDirectory": { - "$ref": "#/$defs/URI" + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -5700,90 +5682,6 @@ "channel" ] }, - "AddWorkspaceFolderParams": { - "type": "object", - "description": "Grants the session's agent tool access to a working directory, adding it to\nthe session's set of {@link CreateSessionParams.workingDirectories | working\ndirectories}. Only valid when the agent advertises\n`multipleWorkspaceFolders`; servers MUST reject this command otherwise.\n\nAll directories in the set are equal peers — there is no privileged\n\"primary\". Adding a directory that is already in the set is a no-op that\nstill returns the current full set.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to grant tool access to." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "RemoveWorkspaceFolderParams": { - "type": "object", - "description": "Revokes the session's agent tool access to one of its working directories.\nThere is no server-side primitive to \"remove\" a single directory mid-session\n— the server instead reconfigures the agent with the reduced directory set\nand returns it, so this command is safe to model as idempotent: removing a\ndirectory that is not in the set is a no-op that still returns the current\nfull set.\n\nA server MAY refuse to remove a directory it cannot relinquish while the\nsession is live (for example one bound to the running agent process),\nreturning an error rather than a result. The protocol itself designates no\ndirectory as special.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to revoke tool access to." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "WorkspaceFolderResult": { - "type": "object", - "description": "Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder`\ncommands: the session's full set of working directories after the mutation.\nAll entries are equal peers; the order carries no meaning.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The session's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "AddWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The session's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "RemoveWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The session's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, "FetchTurnsParams": { "type": "object", "description": "Requests that the host load older historical turns into a chat state.\n\nThe command result does not carry turns. Instead, before responding, the host\nMUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat\nchannel's `turns` state, ahead of the already-loaded window, and update or\nclear `turnsNextCursor`.\n\nBefore applying any operation that references a turn outside the currently\nloaded window, the host MUST eagerly load enough older turns into state for\nthat operation to reduce against valid state.", @@ -5940,90 +5838,6 @@ "channel" ] }, - "AddChatWorkspaceFolderParams": { - "type": "object", - "description": "Grants this chat's agent tool access to a working directory, adding it to\nthe chat's {@link ChatState.workingDirectories | `workingDirectories`} subset.\nThe directory MUST already be present in the owning session's\n`workingDirectories`; servers MUST reject with `InvalidParams` otherwise.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`. Adding a\ndirectory already in the chat's set is a no-op that still returns the current\nfull set.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to grant tool access to. Must be in the session's `workingDirectories`." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "RemoveChatWorkspaceFolderParams": { - "type": "object", - "description": "Revokes this chat's agent tool access to one of its working directories.\nAnalogous to the session-level `removeWorkspaceFolder`: the server\nreconfigures the chat to the reduced subset and returns it. Removing a\ndirectory not in the chat's set is a no-op that still returns the current\nfull set.\n\nOnly valid when the agent advertises `multipleWorkspaceFolders`.", - "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." - }, - "folder": { - "$ref": "#/$defs/URI", - "description": "Directory to revoke tool access to." - } - }, - "required": [ - "channel", - "folder" - ] - }, - "ChatWorkspaceFolderResult": { - "type": "object", - "description": "Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`:\nthe chat's full working-directory subset after the mutation.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The chat's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "AddChatWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The chat's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, - "RemoveChatWorkspaceFolderResult": { - "type": "object", - "description": "Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}.", - "properties": { - "directories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The chat's working directories after the mutation." - } - }, - "required": [ - "directories" - ] - }, "CreateTerminalParams": { "type": "object", "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", @@ -6781,6 +6595,12 @@ { "$ref": "#/$defs/SessionActiveClientRemovedAction" }, + { + "$ref": "#/$defs/SessionWorkingDirectorySetAction" + }, + { + "$ref": "#/$defs/SessionWorkingDirectoryRemovedAction" + }, { "$ref": "#/$defs/SessionInputNeededSetAction" }, @@ -6868,6 +6688,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatWorkingDirectorySetAction" + }, + { + "$ref": "#/$defs/ChatWorkingDirectoryRemovedAction" + }, { "$ref": "#/$defs/ChatUsageAction" }, @@ -7257,10 +7083,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -7365,6 +7187,40 @@ "clientId" ] }, + "SessionWorkingDirectorySetAction": { + "type": "object", + "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkspaceFolders}.", + "properties": { + "type": { + "const": "session/workingDirectorySet" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to grant the session's agent tool access to." + } + }, + "required": [ + "type", + "directory" + ] + }, + "SessionWorkingDirectoryRemovedAction": { + "type": "object", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "properties": { + "type": { + "const": "session/workingDirectoryRemoved" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to revoke the session's agent tool access to." + } + }, + "required": [ + "type", + "directory" + ] + }, "SessionInputNeededSetAction": { "type": "object", "description": "A session-level input request was added or updated.\n\nUpsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the\nhost dispatches this with the full {@link SessionInputRequest} to append a new\nentry to {@link SessionState.inputNeeded} or replace the existing entry with\nthe same `id`.\n\nServer-originated: the host mirrors chat-level requests (elicitations, tool\nconfirmations, client-tool executions) into the session aggregate so clients\nsubscribed only to the session channel can discover them. Clients respond by\ndispatching the ordinary `chat/*` action to the entry's `chat` channel — see\n{@link SessionInputRequest}.", @@ -8105,6 +7961,40 @@ "type" ] }, + "ChatWorkingDirectorySetAction": { + "type": "object", + "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}.", + "properties": { + "type": { + "const": "chat/workingDirectorySet" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to add to this chat's subset." + } + }, + "required": [ + "type", + "directory" + ] + }, + "ChatWorkingDirectoryRemovedAction": { + "type": "object", + "description": "A working directory was removed from this chat's\n{@link ChatState.workingDirectories} subset.\n\nRemoves `directory` from the chat's subset; a no-op when it is not present.\nIdempotent, mirroring `session/workingDirectoryRemoved`. Only affects the\nchat's subset — the directory remains in the session's set.", + "properties": { + "type": { + "const": "chat/workingDirectoryRemoved" + }, + "directory": { + "$ref": "#/$defs/URI", + "description": "The working directory to remove from this chat's subset." + } + }, + "required": [ + "type", + "directory" + ] + }, "ChatUsageAction": { "type": "object", "description": "Token usage report for a turn.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 2e472c161..ff6f444fa 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -98,16 +98,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -797,7 +793,7 @@ }, "multipleWorkspaceFolders": { "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -813,8 +809,13 @@ }, "MultipleWorkspaceFoldersCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", - "properties": {} + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "properties": { + "immutablePrimary": { + "type": "boolean", + "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + } + } }, "SessionModelInfo": { "type": "object", @@ -930,16 +931,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -976,16 +973,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1052,7 +1045,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworkingDirectory." + "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworking directories." } }, "required": [ @@ -1230,7 +1223,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectories`: the session-level set. Individual chats MAY restrict\n to a subset via {@link ChatSummary.workingDirectories}; aggregating these\n up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -1252,16 +1245,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2487,16 +2476,12 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, "turns": { "type": "array", @@ -2581,10 +2566,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -4553,7 +4534,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." + "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/schema/state.schema.json b/schema/state.schema.json index 1ece34d7e..f71ef3232 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -541,7 +541,7 @@ }, "multipleWorkspaceFolders": { "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory, with all directories treated as equal peers (no primary). When\nabsent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder`\nand MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -557,8 +557,13 @@ }, "MultipleWorkspaceFoldersCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.\nCurrently carries no sub-options; presence of an empty object `{}` is the\nentire signal.", - "properties": {} + "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "properties": { + "immutablePrimary": { + "type": "boolean", + "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + } + } }, "SessionModelInfo": { "type": "object", @@ -674,16 +679,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -720,16 +721,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -796,7 +793,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworkingDirectory." + "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworking directories." } }, "required": [ @@ -974,7 +971,7 @@ }, "SessionSummary": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectory` / `workingDirectories`: the session-level set. Individual\n chats MAY restrict to a subset via {@link ChatSummary.workingDirectories};\n aggregating these up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectories`: the session-level set. Individual chats MAY restrict\n to a subset via {@link ChatSummary.workingDirectories}; aggregating these\n up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -996,16 +993,12 @@ "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The full set of working directories the session's agent has tool access\nto, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All\nentries are equal peers — there is no privileged \"primary\". Individual\nchats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}.\n\nWhen absent, fall back to {@link workingDirectory} (if set) as a\nsingle-entry set." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2231,16 +2224,12 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder`\ncalls that violate this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nUse `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the\nset on a running chat." + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, "turns": { "type": "array", @@ -2325,10 +2314,6 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Optional per-chat working directory." - }, "workingDirectories": { "type": "array", "items": { @@ -4297,7 +4282,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to, when it covers changes\nwithin a single directory of a multiroot session. MUST be one of the\nsession's {@link SessionState.workingDirectories}.\n\nA host with multiple working directories groups changes by directory by\nadvertising one changeset per directory, each carrying its\n`workingDirectory` — rather than nesting changes as arrays-of-arrays.\nOmit for changesets that are not directory-scoped (e.g. a single-directory\nsession, or a session-wide roll-up spanning every directory)." + "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index af0e2e8c0..6a03a9119 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1287,6 +1287,10 @@ const ACTION_VARIANTS: { { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', variantName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', variantName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/workingDirectorySet', variantName: 'SessionWorkingDirectorySet', tsInterface: 'SessionWorkingDirectorySetAction' }, + { type: 'session/workingDirectoryRemoved', variantName: 'SessionWorkingDirectoryRemoved', tsInterface: 'SessionWorkingDirectoryRemovedAction' }, + { type: 'chat/workingDirectorySet', variantName: 'ChatWorkingDirectorySet', tsInterface: 'ChatWorkingDirectorySetAction' }, + { type: 'chat/workingDirectoryRemoved', variantName: 'ChatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' }, { type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, { type: 'session/inputNeededRemoved', variantName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, @@ -1436,11 +1440,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'SubscribeParams' }, { name: 'SubscribeView' }, { name: 'SubscriptionDeliveryOptions' }, { name: 'SubscribeResult' }, { name: 'SessionForkSource' }, { name: 'CreateSessionParams' }, { name: 'DisposeSessionParams' }, - { name: 'AddWorkspaceFolderParams' }, { name: 'RemoveWorkspaceFolderParams' }, - { name: 'WorkspaceFolderResult' }, { name: 'AddWorkspaceFolderResult' }, { name: 'RemoveWorkspaceFolderResult' }, { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' }, - { name: 'AddChatWorkspaceFolderParams' }, { name: 'RemoveChatWorkspaceFolderParams' }, - { name: 'ChatWorkspaceFolderResult' }, { name: 'AddChatWorkspaceFolderResult' }, { name: 'RemoveChatWorkspaceFolderResult' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, { name: 'ResourceReadParams' }, { name: 'ResourceReadResult' }, { name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 8d543168f..ea28256a4 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1199,6 +1199,10 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/serverToolsChanged', caseName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', caseName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', caseName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/workingDirectorySet', caseName: 'SessionWorkingDirectorySet', tsInterface: 'SessionWorkingDirectorySetAction' }, + { type: 'session/workingDirectoryRemoved', caseName: 'SessionWorkingDirectoryRemoved', tsInterface: 'SessionWorkingDirectoryRemovedAction' }, + { type: 'chat/workingDirectorySet', caseName: 'ChatWorkingDirectorySet', tsInterface: 'ChatWorkingDirectorySetAction' }, + { type: 'chat/workingDirectoryRemoved', caseName: 'ChatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' }, { type: 'session/inputNeededSet', caseName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, { type: 'session/inputNeededRemoved', caseName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'chat/pendingMessageSet', caseName: 'ChatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, @@ -1410,11 +1414,7 @@ const COMMAND_STRUCTS = [ 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', - 'AddWorkspaceFolderParams', 'RemoveWorkspaceFolderParams', - 'WorkspaceFolderResult', 'AddWorkspaceFolderResult', 'RemoveWorkspaceFolderResult', 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', - 'AddChatWorkspaceFolderParams', 'RemoveChatWorkspaceFolderParams', - 'ChatWorkspaceFolderResult', 'AddChatWorkspaceFolderResult', 'RemoveChatWorkspaceFolderResult', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', 'ResourceWriteParams', 'ResourceWriteResult', @@ -1774,12 +1774,6 @@ object AhpCommands { fun disposeSession(id: Long, params: DisposeSessionParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "disposeSession", params = params) - fun addWorkspaceFolder(id: Long, params: AddWorkspaceFolderParams): JsonRpcRequest = - JsonRpcRequest(id = id, method = "addWorkspaceFolder", params = params) - - fun removeWorkspaceFolder(id: Long, params: RemoveWorkspaceFolderParams): JsonRpcRequest = - JsonRpcRequest(id = id, method = "removeWorkspaceFolder", params = params) - fun listSessions(id: Long, params: ListSessionsParams): JsonRpcRequest = JsonRpcRequest(id = id, method = "listSessions", params = params) diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 3856a6388..04da0303d 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1191,6 +1191,10 @@ const ACTION_VARIANTS: { { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', variantName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', variantName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/workingDirectorySet', variantName: 'SessionWorkingDirectorySet', tsInterface: 'SessionWorkingDirectorySetAction' }, + { type: 'session/workingDirectoryRemoved', variantName: 'SessionWorkingDirectoryRemoved', tsInterface: 'SessionWorkingDirectoryRemovedAction' }, + { type: 'chat/workingDirectorySet', variantName: 'ChatWorkingDirectorySet', tsInterface: 'ChatWorkingDirectorySetAction' }, + { type: 'chat/workingDirectoryRemoved', variantName: 'ChatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' }, { type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction', boxed: true }, { type: 'session/inputNeededRemoved', variantName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'chat/pendingMessageSet', variantName: 'ChatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, @@ -1385,12 +1389,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'SubscribeParams' }, { name: 'SubscribeView' }, { name: 'SubscriptionDeliveryOptions' }, { name: 'SubscribeResult' }, { name: 'SessionForkSource' }, { name: 'CreateSessionParams' }, { name: 'DisposeSessionParams' }, - { name: 'AddWorkspaceFolderParams' }, { name: 'RemoveWorkspaceFolderParams' }, - { name: 'WorkspaceFolderResult' }, { name: 'AddWorkspaceFolderResult' }, { name: 'RemoveWorkspaceFolderResult' }, { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' }, - { name: 'AddChatWorkspaceFolderParams' }, { name: 'RemoveChatWorkspaceFolderParams' }, - { name: 'ChatWorkspaceFolderResult' }, { name: 'AddChatWorkspaceFolderResult' }, { name: 'RemoveChatWorkspaceFolderResult' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, { name: 'ResourceReadParams' }, { name: 'ResourceReadResult' }, { name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 6770cabfe..062ea0210 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1108,6 +1108,10 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/serverToolsChanged', caseName: 'sessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', caseName: 'sessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', caseName: 'sessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/workingDirectorySet', caseName: 'sessionWorkingDirectorySet', tsInterface: 'SessionWorkingDirectorySetAction' }, + { type: 'session/workingDirectoryRemoved', caseName: 'sessionWorkingDirectoryRemoved', tsInterface: 'SessionWorkingDirectoryRemovedAction' }, + { type: 'chat/workingDirectorySet', caseName: 'chatWorkingDirectorySet', tsInterface: 'ChatWorkingDirectorySetAction' }, + { type: 'chat/workingDirectoryRemoved', caseName: 'chatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' }, { type: 'session/inputNeededSet', caseName: 'sessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, { type: 'session/inputNeededRemoved', caseName: 'sessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'chat/pendingMessageSet', caseName: 'chatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, @@ -1328,11 +1332,7 @@ const COMMAND_STRUCTS = [ 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', - 'AddWorkspaceFolderParams', 'RemoveWorkspaceFolderParams', - 'WorkspaceFolderResult', 'AddWorkspaceFolderResult', 'RemoveWorkspaceFolderResult', 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', - 'AddChatWorkspaceFolderParams', 'RemoveChatWorkspaceFolderParams', - 'ChatWorkspaceFolderResult', 'AddChatWorkspaceFolderResult', 'RemoveChatWorkspaceFolderResult', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', 'ResourceWriteParams', 'ResourceWriteResult', @@ -1691,14 +1691,6 @@ public enum AHPCommands { JsonRpcRequest(id: id, method: "disposeSession", params: params) } - public static func addWorkspaceFolder(id: Int, params: AddWorkspaceFolderParams) -> JsonRpcRequest { - JsonRpcRequest(id: id, method: "addWorkspaceFolder", params: params) - } - - public static func removeWorkspaceFolder(id: Int, params: RemoveWorkspaceFolderParams) -> JsonRpcRequest { - JsonRpcRequest(id: id, method: "removeWorkspaceFolder", params: params) - } - public static func listSessions(id: Int, params: ListSessionsParams) -> JsonRpcRequest { JsonRpcRequest(id: id, method: "listSessions", params: params) } diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index e5a6179a9..9ae9d7837 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -17,6 +17,8 @@ import type { SessionServerToolsChangedAction, SessionActiveClientSetAction, SessionActiveClientRemovedAction, + SessionWorkingDirectorySetAction, + SessionWorkingDirectoryRemovedAction, SessionInputNeededSetAction, SessionInputNeededRemovedAction, SessionCustomizationsChangedAction, @@ -46,6 +48,8 @@ import type { ChatTurnCancelledAction, ChatErrorAction, ChatActivityChangedAction, + ChatWorkingDirectorySetAction, + ChatWorkingDirectoryRemovedAction, ChatUsageAction, ChatReasoningAction, ChatPendingMessageSetAction, @@ -120,6 +124,8 @@ export type SessionAction = | SessionServerToolsChangedAction | SessionActiveClientSetAction | SessionActiveClientRemovedAction + | SessionWorkingDirectorySetAction + | SessionWorkingDirectoryRemovedAction | SessionInputNeededSetAction | SessionInputNeededRemovedAction | SessionCustomizationsChangedAction @@ -142,6 +148,8 @@ export type ClientSessionAction = | SessionTitleChangedAction | SessionActiveClientSetAction | SessionActiveClientRemovedAction + | SessionWorkingDirectorySetAction + | SessionWorkingDirectoryRemovedAction | SessionCustomizationToggledAction | SessionMcpServerStartRequestedAction | SessionMcpServerStopRequestedAction @@ -186,6 +194,8 @@ export type ChatAction = | ChatTurnCancelledAction | ChatErrorAction | ChatActivityChangedAction + | ChatWorkingDirectorySetAction + | ChatWorkingDirectoryRemovedAction | ChatUsageAction | ChatReasoningAction | ChatPendingMessageSetAction @@ -207,6 +217,8 @@ export type ClientChatAction = | ChatToolCallResultConfirmedAction | ChatToolCallContentChangedAction | ChatTurnCancelledAction + | ChatWorkingDirectorySetAction + | ChatWorkingDirectoryRemovedAction | ChatPendingMessageSetAction | ChatPendingMessageRemovedAction | ChatQueuedMessagesReorderedAction @@ -353,6 +365,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.SessionServerToolsChanged]: false, [ActionType.SessionActiveClientSet]: true, [ActionType.SessionActiveClientRemoved]: true, + [ActionType.SessionWorkingDirectorySet]: true, + [ActionType.SessionWorkingDirectoryRemoved]: true, [ActionType.SessionInputNeededSet]: false, [ActionType.SessionInputNeededRemoved]: false, [ActionType.SessionCustomizationsChanged]: false, @@ -382,6 +396,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.ChatTurnCancelled]: true, [ActionType.ChatError]: false, [ActionType.ChatActivityChanged]: false, + [ActionType.ChatWorkingDirectorySet]: true, + [ActionType.ChatWorkingDirectoryRemoved]: true, [ActionType.ChatUsage]: false, [ActionType.ChatReasoning]: false, [ActionType.ChatPendingMessageSet]: true, diff --git a/types/channels-changeset/state.ts b/types/channels-changeset/state.ts index 0aec08885..6adc4bfed 100644 --- a/types/channels-changeset/state.ts +++ b/types/channels-changeset/state.ts @@ -67,15 +67,24 @@ export interface Changeset { */ changeKind: string; /** - * The working directory this changeset is scoped to, when it covers changes - * within a single directory of a multiroot session. MUST be one of the - * session's {@link SessionState.workingDirectories}. + * The working directory this changeset is scoped to. When set, it MUST be one + * of the owning session's {@link SessionState.workingDirectories}, and every + * file in the changeset belongs to that directory. * - * A host with multiple working directories groups changes by directory by - * advertising one changeset per directory, each carrying its - * `workingDirectory` — rather than nesting changes as arrays-of-arrays. - * Omit for changesets that are not directory-scoped (e.g. a single-directory - * session, or a session-wide roll-up spanning every directory). + * **Grouping is the host's responsibility, not the client's.** A host whose + * session has multiple working directories MUST group changes by directory — + * emitting one changeset per working directory, each carrying its + * `workingDirectory` — so that a client never has to derive a file's owning + * directory itself (e.g. by prefix-matching URIs, which the client cannot do + * correctly across nested repositories, symlinks, or submodules). This keeps + * AHP a display-ready presentation model (see the + * {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS + * knowledge and hands clients pre-grouped changesets. + * + * Omit only for changesets that are genuinely not scoped to a single working + * directory — e.g. a single-directory session (nothing to group), or an + * aggregate roll-up / out-of-tree changeset that intentionally spans (or sits + * outside) the working directories. */ workingDirectory?: URI; /** diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index c1f5e6634..b642c9af9 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -5,7 +5,7 @@ */ import { ActionType } from '../common/actions.js'; -import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo } from '../common/state.js'; +import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo, URI } from '../common/state.js'; import type { Message, ResponsePart, @@ -443,6 +443,45 @@ export interface ChatActivityChangedAction { activity?: string; } +/** + * A working directory was added to this chat's + * {@link ChatState.workingDirectories} subset. + * + * Membership semantics keyed by the directory URI: the reducer appends + * `directory` when the chat's subset does not already contain it (creating the + * subset if absent) and is a no-op when it is already present. `directory` MUST + * be one of the owning session's {@link SessionState.workingDirectories}; a host + * MUST reject a directory that is not. Only valid when the agent advertises + * {@link AgentCapabilities.multipleWorkspaceFolders}. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatWorkingDirectorySetAction { + type: ActionType.ChatWorkingDirectorySet; + /** The working directory to add to this chat's subset. */ + directory: URI; +} + +/** + * A working directory was removed from this chat's + * {@link ChatState.workingDirectories} subset. + * + * Removes `directory` from the chat's subset; a no-op when it is not present. + * Idempotent, mirroring `session/workingDirectoryRemoved`. Only affects the + * chat's subset — the directory remains in the session's set. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatWorkingDirectoryRemovedAction { + type: ActionType.ChatWorkingDirectoryRemoved; + /** The working directory to remove from this chat's subset. */ + directory: URI; +} + /** * Token usage report for a turn. * @@ -703,6 +742,8 @@ export type ChatAction = | ChatTurnCancelledAction | ChatErrorAction | ChatActivityChangedAction + | ChatWorkingDirectorySetAction + | ChatWorkingDirectoryRemovedAction | ChatUsageAction | ChatReasoningAction | ChatTruncatedAction diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index a23be4eb4..745df0bde 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -1,6 +1,5 @@ /** - * Chat Channel Commands — `createChat`, `disposeChat`, - * `addChatWorkspaceFolder`, and `removeChatWorkspaceFolder`. + * Chat Channel Commands — `createChat` and `disposeChat`. * * @module channels-chat/commands */ @@ -64,67 +63,3 @@ export interface CreateChatParams extends BaseParams { * @version 1 */ export interface DisposeChatParams extends BaseParams {} - -// ─── addChatWorkspaceFolder ────────────────────────────────────────────────── - -/** - * Grants this chat's agent tool access to a working directory, adding it to - * the chat's {@link ChatState.workingDirectories | `workingDirectories`} subset. - * The directory MUST already be present in the owning session's - * `workingDirectories`; servers MUST reject with `InvalidParams` otherwise. - * - * Only valid when the agent advertises `multipleWorkspaceFolders`. Adding a - * directory already in the chat's set is a no-op that still returns the current - * full set. - * - * @category Commands - * @method addChatWorkspaceFolder - * @direction Client → Server - * @messageType Request - * @version 1 - */ -export interface AddChatWorkspaceFolderParams extends BaseParams { - /** Directory to grant tool access to. Must be in the session's `workingDirectories`. */ - folder: URI; -} - -// ─── removeChatWorkspaceFolder ─────────────────────────────────────────────── - -/** - * Revokes this chat's agent tool access to one of its working directories. - * Analogous to the session-level `removeWorkspaceFolder`: the server - * reconfigures the chat to the reduced subset and returns it. Removing a - * directory not in the chat's set is a no-op that still returns the current - * full set. - * - * Only valid when the agent advertises `multipleWorkspaceFolders`. - * - * @category Commands - * @method removeChatWorkspaceFolder - * @direction Client → Server - * @messageType Request - * @version 1 - */ -export interface RemoveChatWorkspaceFolderParams extends BaseParams { - /** Directory to revoke tool access to. */ - folder: URI; -} - -/** - * Result shared by `addChatWorkspaceFolder` and `removeChatWorkspaceFolder`: - * the chat's full working-directory subset after the mutation. - */ -export interface ChatWorkspaceFolderResult { - /** The chat's working directories after the mutation. */ - directories: URI[]; -} - -/** - * Result of the `addChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. - */ -export interface AddChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} - -/** - * Result of the `removeChatWorkspaceFolder` command. See {@link ChatWorkspaceFolderResult}. - */ -export interface RemoveChatWorkspaceFolderResult extends ChatWorkspaceFolderResult {} diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts index 1da23f9a6..480aede9e 100644 --- a/types/channels-chat/reducer.ts +++ b/types/channels-chat/reducer.ts @@ -339,6 +339,30 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st case ActionType.ChatActivityChanged: return { ...state, activity: action.activity }; + // ── Working Directories ─────────────────────────────────────────────── + + case ActionType.ChatWorkingDirectorySet: { + const list = state.workingDirectories ?? []; + if (list.includes(action.directory)) { + return state; + } + return { ...state, workingDirectories: [...list, action.directory] }; + } + + case ActionType.ChatWorkingDirectoryRemoved: { + const list = state.workingDirectories; + if (!list) { + return state; + } + const idx = list.indexOf(action.directory); + if (idx < 0) { + return state; + } + const updated = list.slice(); + updated.splice(idx, 1); + return { ...state, workingDirectories: updated }; + } + // ── Tool Call State Machine ─────────────────────────────────────────── case ActionType.ChatToolCallStart: diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index b5d053d46..1217f0b93 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -57,27 +57,18 @@ export interface ChatState { * compatibility. */ interactivity?: ChatInteractivity; - /** - * Optional per-chat working directory. - * - * @deprecated Use {@link workingDirectories} instead. Retained as a - * single-directory shorthand for backwards compatibility. When - * `workingDirectories` is present this field reflects its first entry so - * older clients continue to see a usable value. - */ - workingDirectory?: URI; /** * The subset of the session's * {@link SessionState.workingDirectories | `workingDirectories`} that this * chat's agent has tool access to. Every entry MUST be present in the owning - * session's `workingDirectories`; servers MUST reject `addChatWorkspaceFolder` - * calls that violate this constraint. + * session's `workingDirectories`; servers MUST reject a + * `chat/workingDirectorySet` action that violates this constraint. * * When absent, the chat inherits the full session set. When present but empty * (not recommended), the chat has no working-directory tool access at all. * - * Use `addChatWorkspaceFolder` / `removeChatWorkspaceFolder` to update the - * set on a running chat. + * Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to + * update the subset on a running chat. */ workingDirectories?: URI[]; @@ -149,14 +140,6 @@ export interface ChatSummary { * compatibility. */ interactivity?: ChatInteractivity; - /** - * Optional per-chat working directory. - * - * @deprecated Use {@link workingDirectories} instead. Retained as a - * single-directory shorthand for backwards compatibility. When - * `workingDirectories` is present this field reflects its first entry. - */ - workingDirectory?: URI; /** * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index fb1841f5b..cd329547a 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -112,9 +112,12 @@ export interface AgentCapabilities { multipleChats?: MultipleChatsCapability; /** * The session's agent can be granted tool access to more than one working - * directory, with all directories treated as equal peers (no primary). When - * absent, clients MUST NOT call `addWorkspaceFolder` / `removeWorkspaceFolder` - * and MUST NOT set more than one entry in + * directory. The directories are treated as equal peers except where the + * agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + * (some backends pin their first directory as a fixed process root). + * + * When absent, clients MUST NOT mutate a session's or chat's working-directory + * set and MUST NOT set more than one entry in * {@link CreateSessionParams.workingDirectories}. */ multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability; @@ -136,12 +139,24 @@ export interface MultipleChatsCapability { /** * Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. - * Currently carries no sub-options; presence of an empty object `{}` is the - * entire signal. * * @category Root State */ -export interface MultipleWorkspaceFoldersCapability {} +export interface MultipleWorkspaceFoldersCapability { + /** + * The agent's **first** working directory (index `0` of + * {@link CreateSessionParams.workingDirectories}) is an immutable primary: + * it is fixed for the lifetime of the session — clients MUST NOT remove or + * reorder it. Additional directories after it remain equal peers that can be + * added and removed freely. + * + * Advertised by backends whose agent process is rooted at a single directory + * that cannot change once the session has started (e.g. the SDK's primary + * `workingDirectory`). When absent or `false`, all directories are equal + * peers and any of them may be removed. + */ + immutablePrimary?: boolean; +} /** * @category Root State diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index b3d4bc338..d40240b46 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -255,6 +255,48 @@ export interface SessionActiveClientRemovedAction { clientId: string; } +// ─── Working Directory Actions ─────────────────────────────────────────────── + +/** + * A working directory was added to the session's + * {@link SessionState.workingDirectories} set. + * + * Membership semantics keyed by the directory URI: the reducer appends + * `directory` when the set does not already contain it (creating the set if + * absent) and is a no-op when it is already present. Only valid when the agent + * advertises {@link AgentCapabilities.multipleWorkspaceFolders}. + * + * @category Session Actions + * @version 1 + * @clientDispatchable + */ +export interface SessionWorkingDirectorySetAction { + type: ActionType.SessionWorkingDirectorySet; + /** The working directory to grant the session's agent tool access to. */ + directory: URI; +} + +/** + * A working directory was removed from the session's + * {@link SessionState.workingDirectories} set. + * + * Removes `directory` from the set; a no-op when it is not present. There is no + * atomic backend "remove one" primitive — a host reconfigures its agent to the + * reduced set — so this action is safe to model as idempotent. A host MAY + * decline to apply the removal (e.g. an immutable primary directory, see + * {@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves + * the set unchanged. + * + * @category Session Actions + * @version 1 + * @clientDispatchable + */ +export interface SessionWorkingDirectoryRemovedAction { + type: ActionType.SessionWorkingDirectoryRemoved; + /** The working directory to revoke the session's agent tool access to. */ + directory: URI; +} + // ─── Input Needed Actions ──────────────────────────────────────────────────── /** diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index d8b483040..6acf1fc94 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -68,26 +68,22 @@ export interface CreateSessionParams extends BaseParams { provider?: string; /** * The working directories the session's agent is granted tool access to. - * A session may span multiple directories, all of which are equal peers — - * there is no privileged "primary" directory. + * A session may span multiple directories; they are equal peers except when + * the agent advertises + * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + * the first entry is a fixed process root). * * A client MUST NOT supply more than one entry unless the agent advertises * {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that * capability treats only the first entry as the session's working directory - * and ignores the rest. Use `addWorkspaceFolder` / `removeWorkspaceFolder` - * to change the set after the session has started. + * and ignores the rest. Dispatch `session/workingDirectorySet` / + * `session/workingDirectoryRemoved` to change the set after the session has + * started. * * Ignored for forked sessions — a fork inherits its working directories * from the source session identified by `fork`. */ workingDirectories?: URI[]; - /** - * @deprecated Use {@link workingDirectories} instead. Retained as a - * single-directory shorthand for backwards compatibility: when set, it is - * equivalent to `workingDirectories: [workingDirectory]`. If both are - * provided, `workingDirectories` takes precedence and this field is ignored. - */ - workingDirectory?: URI; /** * Fork from an existing session. The new session is populated with content * from the source session up to and including the specified turn's response. @@ -137,75 +133,6 @@ export interface CreateSessionParams extends BaseParams { */ export interface DisposeSessionParams extends BaseParams {} -// ─── addWorkspaceFolder ────────────────────────────────────────────────────── - -/** - * Grants the session's agent tool access to a working directory, adding it to - * the session's set of {@link CreateSessionParams.workingDirectories | working - * directories}. Only valid when the agent advertises - * `multipleWorkspaceFolders`; servers MUST reject this command otherwise. - * - * All directories in the set are equal peers — there is no privileged - * "primary". Adding a directory that is already in the set is a no-op that - * still returns the current full set. - * - * @category Commands - * @method addWorkspaceFolder - * @direction Client → Server - * @messageType Request - * @version 1 - */ -export interface AddWorkspaceFolderParams extends BaseParams { - /** Directory to grant tool access to. */ - folder: URI; -} - -// ─── removeWorkspaceFolder ─────────────────────────────────────────────────── - -/** - * Revokes the session's agent tool access to one of its working directories. - * There is no server-side primitive to "remove" a single directory mid-session - * — the server instead reconfigures the agent with the reduced directory set - * and returns it, so this command is safe to model as idempotent: removing a - * directory that is not in the set is a no-op that still returns the current - * full set. - * - * A server MAY refuse to remove a directory it cannot relinquish while the - * session is live (for example one bound to the running agent process), - * returning an error rather than a result. The protocol itself designates no - * directory as special. - * - * @category Commands - * @method removeWorkspaceFolder - * @direction Client → Server - * @messageType Request - * @version 1 - */ -export interface RemoveWorkspaceFolderParams extends BaseParams { - /** Directory to revoke tool access to. */ - folder: URI; -} - -/** - * Result shared by the `addWorkspaceFolder` and `removeWorkspaceFolder` - * commands: the session's full set of working directories after the mutation. - * All entries are equal peers; the order carries no meaning. - */ -export interface WorkspaceFolderResult { - /** The session's working directories after the mutation. */ - directories: URI[]; -} - -/** - * Result of the `addWorkspaceFolder` command. See {@link WorkspaceFolderResult}. - */ -export interface AddWorkspaceFolderResult extends WorkspaceFolderResult {} - -/** - * Result of the `removeWorkspaceFolder` command. See {@link WorkspaceFolderResult}. - */ -export interface RemoveWorkspaceFolderResult extends WorkspaceFolderResult {} - // ─── fetchTurns ────────────────────────────────────────────────────────────── /** diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index 7d1700e6a..3847c9f11 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -226,6 +226,30 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: return { ...state, activeClients: updated }; } + // ── Working Directories ───────────────────────────────────────────── + + case ActionType.SessionWorkingDirectorySet: { + const list = state.workingDirectories ?? []; + if (list.includes(action.directory)) { + return state; + } + return { ...state, workingDirectories: [...list, action.directory] }; + } + + case ActionType.SessionWorkingDirectoryRemoved: { + const list = state.workingDirectories; + if (!list) { + return state; + } + const idx = list.indexOf(action.directory); + if (idx < 0) { + return state; + } + const updated = list.slice(); + updated.splice(idx, 1); + return { ...state, workingDirectories: updated }; + } + // ── Input Needed ──────────────────────────────────────────────────── case ActionType.SessionInputNeededSet: { diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 3aaff9075..aa7316330 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -83,26 +83,15 @@ export interface SessionMetadata { /** Server-owned project for this session */ project?: ProjectInfo; /** - * The default working directory URI for this session. Individual chats - * MAY override via {@link ChatSummary.workingDirectory | their own - * `workingDirectory`}; this field acts as the fallback for any chat that - * does not. - * - * @deprecated Use {@link workingDirectories} instead. Retained as a - * single-directory shorthand for backwards compatibility. When - * `workingDirectories` is present this field reflects its first entry so - * older clients continue to see a usable value. - */ - workingDirectory?: URI; - /** - * The full set of working directories the session's agent has tool access - * to, as maintained by `addWorkspaceFolder` / `removeWorkspaceFolder`. All - * entries are equal peers — there is no privileged "primary". Individual - * chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}. - * - * When absent, fall back to {@link workingDirectory} (if set) as a - * single-entry set. + * The working directories the session's agent has tool access to, as + * maintained by the `session/workingDirectorySet` / + * `session/workingDirectoryRemoved` actions. Directories are equal peers + * except when the agent advertises + * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * entry is then a fixed process root). Individual chats MAY restrict to a + * subset via {@link ChatSummary.workingDirectories | their own + * `workingDirectories`}; a chat that sets none operates against this full + * set. */ workingDirectories?: URI[]; /** @@ -207,7 +196,7 @@ export interface SessionState extends SessionMetadata { * * Clients MAY look for well-known keys here to provide enhanced UI. * For example, a `git` key may provide extra git metadata about the session's - * workingDirectory. + * working directories. */ _meta?: Record; } @@ -403,9 +392,9 @@ export interface ProjectInfo { * chat currently driving the promoted status bits when a non-default chat * wins (e.g. the chat that raised `InputNeeded`). * - `modifiedAt`: the max of all chats' `modifiedAt`. - * - `workingDirectory` / `workingDirectories`: the session-level set. Individual - * chats MAY restrict to a subset via {@link ChatSummary.workingDirectories}; - * aggregating these up is meaningless and SHOULD NOT be attempted. + * - `workingDirectories`: the session-level set. Individual chats MAY restrict + * to a subset via {@link ChatSummary.workingDirectories}; aggregating these + * up is meaningless and SHOULD NOT be attempted. * - `changes`: optional roll-up across all chats. Producers MAY sum the * per-chat changeset stats or report the most expensive chat's stats — * whichever is cheaper for the host to compute. diff --git a/types/common/actions.ts b/types/common/actions.ts index 2a476a10f..b464de0d4 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -26,6 +26,8 @@ import type { SessionServerToolsChangedAction, SessionActiveClientSetAction, SessionActiveClientRemovedAction, + SessionWorkingDirectorySetAction, + SessionWorkingDirectoryRemovedAction, SessionInputNeededSetAction, SessionInputNeededRemovedAction, SessionCustomizationsChangedAction, @@ -58,6 +60,8 @@ import type { ChatTurnCancelledAction, ChatErrorAction, ChatActivityChangedAction, + ChatWorkingDirectorySetAction, + ChatWorkingDirectoryRemovedAction, ChatUsageAction, ChatReasoningAction, ChatPendingMessageSetAction, @@ -138,12 +142,16 @@ export const enum ActionType { ChatTurnCancelled = 'chat/turnCancelled', ChatError = 'chat/error', ChatActivityChanged = 'chat/activityChanged', + ChatWorkingDirectorySet = 'chat/workingDirectorySet', + ChatWorkingDirectoryRemoved = 'chat/workingDirectoryRemoved', SessionTitleChanged = 'session/titleChanged', ChatUsage = 'chat/usage', ChatReasoning = 'chat/reasoning', SessionServerToolsChanged = 'session/serverToolsChanged', SessionActiveClientSet = 'session/activeClientSet', SessionActiveClientRemoved = 'session/activeClientRemoved', + SessionWorkingDirectorySet = 'session/workingDirectorySet', + SessionWorkingDirectoryRemoved = 'session/workingDirectoryRemoved', SessionInputNeededSet = 'session/inputNeededSet', SessionInputNeededRemoved = 'session/inputNeededRemoved', ChatPendingMessageSet = 'chat/pendingMessageSet', @@ -245,6 +253,8 @@ export type StateAction = | SessionServerToolsChangedAction | SessionActiveClientSetAction | SessionActiveClientRemovedAction + | SessionWorkingDirectorySetAction + | SessionWorkingDirectoryRemovedAction | SessionInputNeededSetAction | SessionInputNeededRemovedAction | SessionCustomizationsChangedAction @@ -274,6 +284,8 @@ export type StateAction = | ChatTurnCancelledAction | ChatErrorAction | ChatActivityChangedAction + | ChatWorkingDirectorySetAction + | ChatWorkingDirectoryRemovedAction | ChatUsageAction | ChatReasoningAction | ChatPendingMessageSetAction diff --git a/types/common/messages.ts b/types/common/messages.ts index 3a9831d88..e248b9e26 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -49,10 +49,6 @@ import type { import type { CreateSessionParams, DisposeSessionParams, - AddWorkspaceFolderParams, - AddWorkspaceFolderResult, - RemoveWorkspaceFolderParams, - RemoveWorkspaceFolderResult, FetchTurnsParams, FetchTurnsResult, CompletionsParams, @@ -61,10 +57,6 @@ import type { import type { CreateChatParams, DisposeChatParams, - AddChatWorkspaceFolderParams, - AddChatWorkspaceFolderResult, - RemoveChatWorkspaceFolderParams, - RemoveChatWorkspaceFolderResult, } from '../channels-chat/commands.js'; import type { CreateTerminalParams, @@ -161,12 +153,8 @@ export interface CommandMap { 'subscribe': { params: SubscribeParams; result: SubscribeResult }; 'createSession': { params: CreateSessionParams; result: null }; 'disposeSession': { params: DisposeSessionParams; result: null }; - 'addWorkspaceFolder': { params: AddWorkspaceFolderParams; result: AddWorkspaceFolderResult }; - 'removeWorkspaceFolder': { params: RemoveWorkspaceFolderParams; result: RemoveWorkspaceFolderResult }; 'createChat': { params: CreateChatParams; result: null }; 'disposeChat': { params: DisposeChatParams; result: null }; - 'addChatWorkspaceFolder': { params: AddChatWorkspaceFolderParams; result: AddChatWorkspaceFolderResult }; - 'removeChatWorkspaceFolder': { params: RemoveChatWorkspaceFolderParams; result: RemoveChatWorkspaceFolderResult }; 'createTerminal': { params: CreateTerminalParams; result: null }; 'disposeTerminal': { params: DisposeTerminalParams; result: null }; 'createResourceWatch': { params: CreateResourceWatchParams; result: CreateResourceWatchResult }; diff --git a/types/test-cases/reducers/241-session-workingdirectoryset-adds-to-empty-set.json b/types/test-cases/reducers/241-session-workingdirectoryset-adds-to-empty-set.json new file mode 100644 index 000000000..a86218b41 --- /dev/null +++ b/types/test-cases/reducers/241-session-workingdirectoryset-adds-to-empty-set.json @@ -0,0 +1,29 @@ +{ + "description": "session-workingDirectorySet adds to empty set", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/workingDirectorySet", + "directory": "file:///repo-a" + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a" + ] + } +} diff --git a/types/test-cases/reducers/242-session-workingdirectoryset-appends-to-existing-set.json b/types/test-cases/reducers/242-session-workingdirectoryset-appends-to-existing-set.json new file mode 100644 index 000000000..6b459c765 --- /dev/null +++ b/types/test-cases/reducers/242-session-workingdirectoryset-appends-to-existing-set.json @@ -0,0 +1,33 @@ +{ + "description": "session-workingDirectorySet appends to existing set", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a" + ] + }, + "actions": [ + { + "type": "session/workingDirectorySet", + "directory": "file:///repo-b" + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a", + "file:///repo-b" + ] + } +} diff --git a/types/test-cases/reducers/243-session-workingdirectoryset-no-op-when-already-present.json b/types/test-cases/reducers/243-session-workingdirectoryset-no-op-when-already-present.json new file mode 100644 index 000000000..4d6dbbd77 --- /dev/null +++ b/types/test-cases/reducers/243-session-workingdirectoryset-no-op-when-already-present.json @@ -0,0 +1,32 @@ +{ + "description": "session-workingDirectorySet no-op when already present", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a" + ] + }, + "actions": [ + { + "type": "session/workingDirectorySet", + "directory": "file:///repo-a" + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a" + ] + } +} diff --git a/types/test-cases/reducers/244-session-workingdirectoryremoved-removes-existing.json b/types/test-cases/reducers/244-session-workingdirectoryremoved-removes-existing.json new file mode 100644 index 000000000..bbac0bec5 --- /dev/null +++ b/types/test-cases/reducers/244-session-workingdirectoryremoved-removes-existing.json @@ -0,0 +1,33 @@ +{ + "description": "session-workingDirectoryRemoved removes existing", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a", + "file:///repo-b" + ] + }, + "actions": [ + { + "type": "session/workingDirectoryRemoved", + "directory": "file:///repo-a" + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-b" + ] + } +} diff --git a/types/test-cases/reducers/245-session-workingdirectoryremoved-no-op-when-not-in-set.json b/types/test-cases/reducers/245-session-workingdirectoryremoved-no-op-when-not-in-set.json new file mode 100644 index 000000000..75f080629 --- /dev/null +++ b/types/test-cases/reducers/245-session-workingdirectoryremoved-no-op-when-not-in-set.json @@ -0,0 +1,32 @@ +{ + "description": "session-workingDirectoryRemoved no-op when not in set", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a" + ] + }, + "actions": [ + { + "type": "session/workingDirectoryRemoved", + "directory": "file:///repo-b" + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": [ + "file:///repo-a" + ] + } +} diff --git a/types/test-cases/reducers/246-session-workingdirectoryremoved-no-op-when-set-absent.json b/types/test-cases/reducers/246-session-workingdirectoryremoved-no-op-when-set-absent.json new file mode 100644 index 000000000..955bd9215 --- /dev/null +++ b/types/test-cases/reducers/246-session-workingdirectoryremoved-no-op-when-set-absent.json @@ -0,0 +1,26 @@ +{ + "description": "session-workingDirectoryRemoved no-op when set absent", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/workingDirectoryRemoved", + "directory": "file:///repo-a" + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + } +} diff --git a/types/test-cases/reducers/247-chat-workingdirectoryset-adds-to-empty-set.json b/types/test-cases/reducers/247-chat-workingdirectoryset-adds-to-empty-set.json new file mode 100644 index 000000000..5b82e7917 --- /dev/null +++ b/types/test-cases/reducers/247-chat-workingdirectoryset-adds-to-empty-set.json @@ -0,0 +1,27 @@ +{ + "description": "chat-workingDirectorySet adds to empty set", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" + }, + "actions": [ + { + "type": "chat/workingDirectorySet", + "directory": "file:///repo-a" + } + ], + "expected": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a" + ] + } +} diff --git a/types/test-cases/reducers/248-chat-workingdirectoryset-appends-to-existing-set.json b/types/test-cases/reducers/248-chat-workingdirectoryset-appends-to-existing-set.json new file mode 100644 index 000000000..1f962cd80 --- /dev/null +++ b/types/test-cases/reducers/248-chat-workingdirectoryset-appends-to-existing-set.json @@ -0,0 +1,31 @@ +{ + "description": "chat-workingDirectorySet appends to existing set", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a" + ] + }, + "actions": [ + { + "type": "chat/workingDirectorySet", + "directory": "file:///repo-b" + } + ], + "expected": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a", + "file:///repo-b" + ] + } +} diff --git a/types/test-cases/reducers/249-chat-workingdirectoryset-no-op-when-already-present.json b/types/test-cases/reducers/249-chat-workingdirectoryset-no-op-when-already-present.json new file mode 100644 index 000000000..dee5eb6cc --- /dev/null +++ b/types/test-cases/reducers/249-chat-workingdirectoryset-no-op-when-already-present.json @@ -0,0 +1,30 @@ +{ + "description": "chat-workingDirectorySet no-op when already present", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a" + ] + }, + "actions": [ + { + "type": "chat/workingDirectorySet", + "directory": "file:///repo-a" + } + ], + "expected": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a" + ] + } +} diff --git a/types/test-cases/reducers/250-chat-workingdirectoryremoved-removes-existing.json b/types/test-cases/reducers/250-chat-workingdirectoryremoved-removes-existing.json new file mode 100644 index 000000000..60b73b23d --- /dev/null +++ b/types/test-cases/reducers/250-chat-workingdirectoryremoved-removes-existing.json @@ -0,0 +1,31 @@ +{ + "description": "chat-workingDirectoryRemoved removes existing", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a", + "file:///repo-b" + ] + }, + "actions": [ + { + "type": "chat/workingDirectoryRemoved", + "directory": "file:///repo-a" + } + ], + "expected": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-b" + ] + } +} diff --git a/types/test-cases/reducers/251-chat-workingdirectoryremoved-no-op-when-not-in-set.json b/types/test-cases/reducers/251-chat-workingdirectoryremoved-no-op-when-not-in-set.json new file mode 100644 index 000000000..6dd23a805 --- /dev/null +++ b/types/test-cases/reducers/251-chat-workingdirectoryremoved-no-op-when-not-in-set.json @@ -0,0 +1,30 @@ +{ + "description": "chat-workingDirectoryRemoved no-op when not in set", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a" + ] + }, + "actions": [ + { + "type": "chat/workingDirectoryRemoved", + "directory": "file:///repo-b" + } + ], + "expected": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "workingDirectories": [ + "file:///repo-a" + ] + } +} diff --git a/types/test-cases/reducers/252-chat-workingdirectoryremoved-no-op-when-set-absent.json b/types/test-cases/reducers/252-chat-workingdirectoryremoved-no-op-when-set-absent.json new file mode 100644 index 000000000..31b94eea8 --- /dev/null +++ b/types/test-cases/reducers/252-chat-workingdirectoryremoved-no-op-when-set-absent.json @@ -0,0 +1,24 @@ +{ + "description": "chat-workingDirectoryRemoved no-op when set absent", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" + }, + "actions": [ + { + "type": "chat/workingDirectoryRemoved", + "directory": "file:///repo-a" + } + ], + "expected": { + "turns": [], + "resource": "copilot:/test-session/chat-1", + "title": "Test Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" + } +} diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index 38613ea5e..8da4dddf5 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -56,12 +56,8 @@ type _ExpectedCommands = | 'subscribe' | 'createSession' | 'disposeSession' - | 'addWorkspaceFolder' - | 'removeWorkspaceFolder' | 'createChat' | 'disposeChat' - | 'addChatWorkspaceFolder' - | 'removeChatWorkspaceFolder' | 'createTerminal' | 'disposeTerminal' | 'createResourceWatch' diff --git a/types/version/registry.ts b/types/version/registry.ts index 94c7bbfb0..af569a9d1 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -89,6 +89,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionServerToolsChanged]: '0.1.0', [ActionType.SessionActiveClientSet]: '0.5.0', [ActionType.SessionActiveClientRemoved]: '0.5.0', + [ActionType.SessionWorkingDirectorySet]: '0.6.0', + [ActionType.SessionWorkingDirectoryRemoved]: '0.6.0', [ActionType.SessionInputNeededSet]: '0.5.1', [ActionType.SessionInputNeededRemoved]: '0.5.1', [ActionType.SessionCustomizationsChanged]: '0.1.0', @@ -118,6 +120,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChatTurnCancelled]: '0.4.0', [ActionType.ChatError]: '0.4.0', [ActionType.ChatActivityChanged]: '0.5.0', + [ActionType.ChatWorkingDirectorySet]: '0.6.0', + [ActionType.ChatWorkingDirectoryRemoved]: '0.6.0', [ActionType.ChatUsage]: '0.4.0', [ActionType.ChatReasoning]: '0.4.0', [ActionType.ChatPendingMessageSet]: '0.4.0', From 1ebc2066091f962c90c82434755feae95719edcb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 17 Jul 2026 11:32:52 +0200 Subject: [PATCH 08/15] spec: rename capability + drop Changeset.workingDirectory (review round 2) - Rename the capability `multipleWorkspaceFolders` -> `multipleWorkingDirectories` (and `MultipleWorkspaceFoldersCapability` -> `MultipleWorkingDirectoriesCapability`) for consistency with the `workingDirectory` vocabulary used everywhere else (per DonJayamanne). - Drop `Changeset.workingDirectory` and the `'directory'` changeKind. A changeset can span working directories (e.g. a per-turn diff), so a single-directory scalar is the wrong model. Per-directory presentation is optional: clients group a changeset's files against the session's workingDirectories, and a host MAY advertise extra per-directory catalogue entries (the changesets list is already unbounded). Resolves the reviewer thread rather than holding the MUST. - Update state-model / changesets guides and the proposal accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahptypes/actions.generated.go | 6 +- clients/go/ahptypes/commands.generated.go | 6 +- .../go/ahptypes/notifications.generated.go | 2 +- clients/go/ahptypes/state.generated.go | 34 ++----- .../generated/Commands.generated.kt | 6 +- .../generated/Notifications.generated.kt | 2 +- .../generated/State.generated.kt | 34 +------ clients/rust/crates/ahp-types/src/actions.rs | 6 +- clients/rust/crates/ahp-types/src/commands.rs | 6 +- .../crates/ahp-types/src/notifications.rs | 2 +- clients/rust/crates/ahp-types/src/state.rs | 35 ++----- .../Generated/Commands.generated.swift | 6 +- .../Generated/Notifications.generated.swift | 2 +- .../Generated/State.generated.swift | 38 ++----- .../.changes/20260716-multiroot-sessions.json | 2 +- docs/guide/changesets.md | 27 +++-- docs/guide/state-model.md | 10 +- docs/proposals/multiroot-sessions.md | 99 ++++++++++--------- schema/actions.schema.json | 28 +++--- schema/commands.schema.json | 32 +++--- schema/errors.schema.json | 32 +++--- schema/notifications.schema.json | 24 ++--- schema/state.schema.json | 22 ++--- scripts/generate-go.ts | 2 +- scripts/generate-kotlin.ts | 2 +- scripts/generate-rust.ts | 2 +- scripts/generate-swift.ts | 2 +- types/channels-changeset/state.ts | 26 +---- types/channels-chat/actions.ts | 2 +- types/channels-chat/commands.ts | 2 +- types/channels-root/state.ts | 8 +- types/channels-session/actions.ts | 4 +- types/channels-session/commands.ts | 4 +- types/channels-session/state.ts | 2 +- 34 files changed, 191 insertions(+), 326 deletions(-) diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index c8a0e6db7..dbccdab49 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -812,7 +812,7 @@ type SessionActiveClientRemovedAction struct { // Membership semantics keyed by the directory URI: the reducer appends // `directory` when the set does not already contain it (creating the set if // absent) and is a no-op when it is already present. Only valid when the agent -// advertises {@link AgentCapabilities.multipleWorkspaceFolders}. +// advertises {@link AgentCapabilities.multipleWorkingDirectories}. type SessionWorkingDirectorySetAction struct { Type ActionType `json:"type"` // The working directory to grant the session's agent tool access to. @@ -826,7 +826,7 @@ type SessionWorkingDirectorySetAction struct { // atomic backend "remove one" primitive — a host reconfigures its agent to the // reduced set — so this action is safe to model as idempotent. A host MAY // decline to apply the removal (e.g. an immutable primary directory, see -// {@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves +// {@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves // the set unchanged. type SessionWorkingDirectoryRemovedAction struct { Type ActionType `json:"type"` @@ -842,7 +842,7 @@ type SessionWorkingDirectoryRemovedAction struct { // subset if absent) and is a no-op when it is already present. `directory` MUST // be one of the owning session's {@link SessionState.workingDirectories}; a host // MUST reject a directory that is not. Only valid when the agent advertises -// {@link AgentCapabilities.multipleWorkspaceFolders}. +// {@link AgentCapabilities.multipleWorkingDirectories}. type ChatWorkingDirectorySetAction struct { Type ActionType `json:"type"` // The working directory to add to this chat's subset. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 82904ac57..0ed3fc60f 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -308,11 +308,11 @@ type CreateSessionParams struct { // The working directories the session's agent is granted tool access to. // A session may span multiple directories; they are equal peers except when // the agent advertises - // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case // the first entry is a fixed process root). // // A client MUST NOT supply more than one entry unless the agent advertises - // {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + // {@link AgentCapabilities.multipleWorkingDirectories}; a server without that // capability treats only the first entry as the session's working directory // and ignores the rest. Dispatch `session/workingDirectorySet` / // `session/workingDirectoryRemoved` to change the set after the session has @@ -380,7 +380,7 @@ type CreateChatParams struct { // `workingDirectories`; this field is ignored for forked chats. // // A client MUST NOT supply this field unless the agent advertises - // {@link AgentCapabilities.multipleWorkspaceFolders}. + // {@link AgentCapabilities.multipleWorkingDirectories}. WorkingDirectories []URI `json:"workingDirectories,omitempty"` } diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 6c7cb0197..34e0908dd 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -221,7 +221,7 @@ type PartialSessionSummary struct { // maintained by the `session/workingDirectorySet` / // `session/workingDirectoryRemoved` actions. Directories are equal peers // except when the agent advertises - // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first // entry is then a fixed process root). Individual chats MAY restrict to a // subset via {@link ChatSummary.workingDirectories | their own // `workingDirectories`}; a chat that sets none operates against this full diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index db4c3efed..f2fde7ef9 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -565,13 +565,13 @@ type AgentCapabilities struct { MultipleChats *MultipleChatsCapability `json:"multipleChats,omitempty"` // The session's agent can be granted tool access to more than one working // directory. The directories are treated as equal peers except where the - // agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + // agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} // (some backends pin their first directory as a fixed process root). // // When absent, clients MUST NOT mutate a session's or chat's working-directory // set and MUST NOT set more than one entry in // {@link CreateSessionParams.workingDirectories}. - MultipleWorkspaceFolders *MultipleWorkspaceFoldersCapability `json:"multipleWorkspaceFolders,omitempty"` + MultipleWorkingDirectories *MultipleWorkingDirectoriesCapability `json:"multipleWorkingDirectories,omitempty"` } // Options for the {@link AgentCapabilities.multipleChats} capability. @@ -582,8 +582,8 @@ type MultipleChatsCapability struct { Fork *bool `json:"fork,omitempty"` } -// Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. -type MultipleWorkspaceFoldersCapability struct { +// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. +type MultipleWorkingDirectoriesCapability struct { // The agent's **first** working directory (index `0` of // {@link CreateSessionParams.workingDirectories}) is an immutable primary: // it is fixed for the lifetime of the session — clients MUST NOT remove or @@ -721,7 +721,7 @@ type SessionState struct { // maintained by the `session/workingDirectorySet` / // `session/workingDirectoryRemoved` actions. Directories are equal peers // except when the agent advertises - // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first // entry is then a fixed process root). Individual chats MAY restrict to a // subset via {@link ChatSummary.workingDirectories | their own // `workingDirectories`}; a chat that sets none operates against this full @@ -956,7 +956,7 @@ type SessionSummary struct { // maintained by the `session/workingDirectorySet` / // `session/workingDirectoryRemoved` actions. Directories are equal peers // except when the agent advertises - // {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first // entry is then a fixed process root). Individual chats MAY restrict to a // subset via {@link ChatSummary.workingDirectories | their own // `workingDirectories`}; a chat that sets none operates against this full @@ -2972,9 +2972,6 @@ type Changeset struct { // // - `'session'`: a static, session-wide changeset covering all changes the // agent has produced in this session. - // - `'directory'`: all changes the agent has produced within a single - // {@link workingDirectory | working directory} of a multiroot session. - // Paired with {@link workingDirectory}. // - `'branch'`: changes relative to a base branch (e.g. a feature branch // diffed against `main`). // - `'uncommitted'`: the workspace's current uncommitted changes. @@ -2987,25 +2984,6 @@ type Changeset struct { // Implementations MAY provide additional values; clients SHOULD fall back // to a reasonable default when an unknown value is encountered. ChangeKind string `json:"changeKind"` - // The working directory this changeset is scoped to. When set, it MUST be one - // of the owning session's {@link SessionState.workingDirectories}, and every - // file in the changeset belongs to that directory. - // - // **Grouping is the host's responsibility, not the client's.** A host whose - // session has multiple working directories MUST group changes by directory — - // emitting one changeset per working directory, each carrying its - // `workingDirectory` — so that a client never has to derive a file's owning - // directory itself (e.g. by prefix-matching URIs, which the client cannot do - // correctly across nested repositories, symlinks, or submodules). This keeps - // AHP a display-ready presentation model (see the - // {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS - // knowledge and hands clients pre-grouped changesets. - // - // Omit only for changesets that are genuinely not scoped to a single working - // directory — e.g. a single-directory session (nothing to group), or an - // aggregate roll-up / out-of-tree changeset that intentionally spans (or sits - // outside) the working directories. - WorkingDirectory *URI `json:"workingDirectory,omitempty"` // Optional capability declarations for this changeset. Absent (or an empty // object) means the changeset advertises no optional capabilities. // diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 6de973e0e..96d9b60f6 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -372,11 +372,11 @@ data class CreateSessionParams( * The working directories the session's agent is granted tool access to. * A session may span multiple directories; they are equal peers except when * the agent advertises - * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case * the first entry is a fixed process root). * * A client MUST NOT supply more than one entry unless the agent advertises - * {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + * {@link AgentCapabilities.multipleWorkingDirectories}; a server without that * capability treats only the first entry as the session's working directory * and ignores the rest. Dispatch `session/workingDirectorySet` / * `session/workingDirectoryRemoved` to change the set after the session has @@ -466,7 +466,7 @@ data class CreateChatParams( * `workingDirectories`; this field is ignored for forked chats. * * A client MUST NOT supply this field unless the agent advertises - * {@link AgentCapabilities.multipleWorkspaceFolders}. + * {@link AgentCapabilities.multipleWorkingDirectories}. */ val workingDirectories: List? = null ) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 36bb50da6..64e93a151 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -200,7 +200,7 @@ data class PartialSessionSummary( * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first * entry is then a fixed process root). Individual chats MAY restrict to a * subset via {@link ChatSummary.workingDirectories | their own * `workingDirectories`}; a chat that sets none operates against this full diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 333a2ce01..1a147007b 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -924,14 +924,14 @@ data class AgentCapabilities( /** * The session's agent can be granted tool access to more than one working * directory. The directories are treated as equal peers except where the - * agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + * agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} * (some backends pin their first directory as a fixed process root). * * When absent, clients MUST NOT mutate a session's or chat's working-directory * set and MUST NOT set more than one entry in * {@link CreateSessionParams.workingDirectories}. */ - val multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? = null + val multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? = null ) @Serializable @@ -945,7 +945,7 @@ data class MultipleChatsCapability( ) @Serializable -data class MultipleWorkspaceFoldersCapability( +data class MultipleWorkingDirectoriesCapability( /** * The agent's **first** working directory (index `0` of * {@link CreateSessionParams.workingDirectories}) is an immutable primary: @@ -1279,7 +1279,7 @@ data class SessionState( * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first * entry is then a fixed process root). Individual chats MAY restrict to a * subset via {@link ChatSummary.workingDirectories | their own * `workingDirectories`}; a chat that sets none operates against this full @@ -1526,7 +1526,7 @@ data class SessionSummary( * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first * entry is then a fixed process root). Individual chats MAY restrict to a * subset via {@link ChatSummary.workingDirectories | their own * `workingDirectories`}; a chat that sets none operates against this full @@ -4036,9 +4036,6 @@ data class Changeset( * * - `'session'`: a static, session-wide changeset covering all changes the * agent has produced in this session. - * - `'directory'`: all changes the agent has produced within a single - * {@link workingDirectory | working directory} of a multiroot session. - * Paired with {@link workingDirectory}. * - `'branch'`: changes relative to a base branch (e.g. a feature branch * diffed against `main`). * - `'uncommitted'`: the workspace's current uncommitted changes. @@ -4052,27 +4049,6 @@ data class Changeset( * to a reasonable default when an unknown value is encountered. */ val changeKind: String, - /** - * The working directory this changeset is scoped to. When set, it MUST be one - * of the owning session's {@link SessionState.workingDirectories}, and every - * file in the changeset belongs to that directory. - * - * **Grouping is the host's responsibility, not the client's.** A host whose - * session has multiple working directories MUST group changes by directory — - * emitting one changeset per working directory, each carrying its - * `workingDirectory` — so that a client never has to derive a file's owning - * directory itself (e.g. by prefix-matching URIs, which the client cannot do - * correctly across nested repositories, symlinks, or submodules). This keeps - * AHP a display-ready presentation model (see the - * {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS - * knowledge and hands clients pre-grouped changesets. - * - * Omit only for changesets that are genuinely not scoped to a single working - * directory — e.g. a single-directory session (nothing to group), or an - * aggregate roll-up / out-of-tree changeset that intentionally spans (or sits - * outside) the working directories. - */ - val workingDirectory: String? = null, /** * Optional capability declarations for this changeset. Absent (or an empty * object) means the changeset advertises no optional capabilities. diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 4b7008570..4b07bec68 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -860,7 +860,7 @@ pub struct SessionActiveClientRemovedAction { /// Membership semantics keyed by the directory URI: the reducer appends /// `directory` when the set does not already contain it (creating the set if /// absent) and is a no-op when it is already present. Only valid when the agent -/// advertises {@link AgentCapabilities.multipleWorkspaceFolders}. +/// advertises {@link AgentCapabilities.multipleWorkingDirectories}. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionWorkingDirectorySetAction { @@ -875,7 +875,7 @@ pub struct SessionWorkingDirectorySetAction { /// atomic backend "remove one" primitive — a host reconfigures its agent to the /// reduced set — so this action is safe to model as idempotent. A host MAY /// decline to apply the removal (e.g. an immutable primary directory, see -/// {@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves +/// {@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves /// the set unchanged. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -892,7 +892,7 @@ pub struct SessionWorkingDirectoryRemovedAction { /// subset if absent) and is a no-op when it is already present. `directory` MUST /// be one of the owning session's {@link SessionState.workingDirectories}; a host /// MUST reject a directory that is not. Only valid when the agent advertises -/// {@link AgentCapabilities.multipleWorkspaceFolders}. +/// {@link AgentCapabilities.multipleWorkingDirectories}. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatWorkingDirectorySetAction { diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 11b9c2a22..b2b85289d 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -394,11 +394,11 @@ pub struct CreateSessionParams { /// The working directories the session's agent is granted tool access to. /// A session may span multiple directories; they are equal peers except when /// the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case /// the first entry is a fixed process root). /// /// A client MUST NOT supply more than one entry unless the agent advertises - /// {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + /// {@link AgentCapabilities.multipleWorkingDirectories}; a server without that /// capability treats only the first entry as the session's working directory /// and ignores the rest. Dispatch `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` to change the set after the session has @@ -479,7 +479,7 @@ pub struct CreateChatParams { /// `workingDirectories`; this field is ignored for forked chats. /// /// A client MUST NOT supply this field unless the agent advertises - /// {@link AgentCapabilities.multipleWorkspaceFolders}. + /// {@link AgentCapabilities.multipleWorkingDirectories}. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, } diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 92b154b9e..e54e93769 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -252,7 +252,7 @@ pub struct PartialSessionSummary { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first /// entry is then a fixed process root). Individual chats MAY restrict to a /// subset via {@link ChatSummary.workingDirectories | their own /// `workingDirectories`}; a chat that sets none operates against this full diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index adc2064de..a843b570f 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -781,14 +781,14 @@ pub struct AgentCapabilities { pub multiple_chats: Option, /// The session's agent can be granted tool access to more than one working /// directory. The directories are treated as equal peers except where the - /// agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + /// agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} /// (some backends pin their first directory as a fixed process root). /// /// When absent, clients MUST NOT mutate a session's or chat's working-directory /// set and MUST NOT set more than one entry in /// {@link CreateSessionParams.workingDirectories}. #[serde(default, skip_serializing_if = "Option::is_none")] - pub multiple_workspace_folders: Option, + pub multiple_working_directories: Option, } /// Options for the {@link AgentCapabilities.multipleChats} capability. @@ -802,10 +802,10 @@ pub struct MultipleChatsCapability { pub fork: Option, } -/// Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. +/// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub struct MultipleWorkspaceFoldersCapability { +pub struct MultipleWorkingDirectoriesCapability { /// The agent's **first** working directory (index `0` of /// {@link CreateSessionParams.workingDirectories}) is an immutable primary: /// it is fixed for the lifetime of the session — clients MUST NOT remove or @@ -1112,7 +1112,7 @@ pub struct SessionState { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first /// entry is then a fixed process root). Individual chats MAY restrict to a /// subset via {@link ChatSummary.workingDirectories | their own /// `workingDirectories`}; a chat that sets none operates against this full @@ -1368,7 +1368,7 @@ pub struct SessionSummary { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first /// entry is then a fixed process root). Individual chats MAY restrict to a /// subset via {@link ChatSummary.workingDirectories | their own /// `workingDirectories`}; a chat that sets none operates against this full @@ -3607,9 +3607,6 @@ pub struct Changeset { /// /// - `'session'`: a static, session-wide changeset covering all changes the /// agent has produced in this session. - /// - `'directory'`: all changes the agent has produced within a single - /// {@link workingDirectory | working directory} of a multiroot session. - /// Paired with {@link workingDirectory}. /// - `'branch'`: changes relative to a base branch (e.g. a feature branch /// diffed against `main`). /// - `'uncommitted'`: the workspace's current uncommitted changes. @@ -3622,26 +3619,6 @@ pub struct Changeset { /// Implementations MAY provide additional values; clients SHOULD fall back /// to a reasonable default when an unknown value is encountered. pub change_kind: String, - /// The working directory this changeset is scoped to. When set, it MUST be one - /// of the owning session's {@link SessionState.workingDirectories}, and every - /// file in the changeset belongs to that directory. - /// - /// **Grouping is the host's responsibility, not the client's.** A host whose - /// session has multiple working directories MUST group changes by directory — - /// emitting one changeset per working directory, each carrying its - /// `workingDirectory` — so that a client never has to derive a file's owning - /// directory itself (e.g. by prefix-matching URIs, which the client cannot do - /// correctly across nested repositories, symlinks, or submodules). This keeps - /// AHP a display-ready presentation model (see the - /// {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS - /// knowledge and hands clients pre-grouped changesets. - /// - /// Omit only for changesets that are genuinely not scoped to a single working - /// directory — e.g. a single-directory session (nothing to group), or an - /// aggregate roll-up / out-of-tree changeset that intentionally spans (or sits - /// outside) the working directories. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_directory: Option, /// Optional capability declarations for this changeset. Absent (or an empty /// object) means the changeset advertises no optional capabilities. /// diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index f3c63ca92..380570e48 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -357,11 +357,11 @@ public struct CreateSessionParams: Codable, Sendable { /// The working directories the session's agent is granted tool access to. /// A session may span multiple directories; they are equal peers except when /// the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case /// the first entry is a fixed process root). /// /// A client MUST NOT supply more than one entry unless the agent advertises - /// {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + /// {@link AgentCapabilities.multipleWorkingDirectories}; a server without that /// capability treats only the first entry as the session's working directory /// and ignores the rest. Dispatch `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` to change the set after the session has @@ -456,7 +456,7 @@ public struct CreateChatParams: Codable, Sendable { /// `workingDirectories`; this field is ignored for forked chats. /// /// A client MUST NOT supply this field unless the agent advertises - /// {@link AgentCapabilities.multipleWorkspaceFolders}. + /// {@link AgentCapabilities.multipleWorkingDirectories}. public var workingDirectories: [String]? public init( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index f153c0299..4ffbb95c7 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -186,7 +186,7 @@ public struct PartialSessionSummary: Codable, Sendable { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first /// entry is then a fixed process root). Individual chats MAY restrict to a /// subset via {@link ChatSummary.workingDirectories | their own /// `workingDirectories`}; a chat that sets none operates against this full diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index f96402d01..1f9220c5d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -617,20 +617,20 @@ public struct AgentCapabilities: Codable, Sendable { public var multipleChats: MultipleChatsCapability? /// The session's agent can be granted tool access to more than one working /// directory. The directories are treated as equal peers except where the - /// agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + /// agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} /// (some backends pin their first directory as a fixed process root). /// /// When absent, clients MUST NOT mutate a session's or chat's working-directory /// set and MUST NOT set more than one entry in /// {@link CreateSessionParams.workingDirectories}. - public var multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? + public var multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? public init( multipleChats: MultipleChatsCapability? = nil, - multipleWorkspaceFolders: MultipleWorkspaceFoldersCapability? = nil + multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? = nil ) { self.multipleChats = multipleChats - self.multipleWorkspaceFolders = multipleWorkspaceFolders + self.multipleWorkingDirectories = multipleWorkingDirectories } } @@ -647,7 +647,7 @@ public struct MultipleChatsCapability: Codable, Sendable { } } -public struct MultipleWorkspaceFoldersCapability: Codable, Sendable { +public struct MultipleWorkingDirectoriesCapability: Codable, Sendable { /// The agent's **first** working directory (index `0` of /// {@link CreateSessionParams.workingDirectories}) is an immutable primary: /// it is fixed for the lifetime of the session — clients MUST NOT remove or @@ -1042,7 +1042,7 @@ public struct SessionState: Codable, Sendable { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first /// entry is then a fixed process root). Individual chats MAY restrict to a /// subset via {@link ChatSummary.workingDirectories | their own /// `workingDirectories`}; a chat that sets none operates against this full @@ -1330,7 +1330,7 @@ public struct SessionSummary: Codable, Sendable { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first /// entry is then a fixed process root). Individual chats MAY restrict to a /// subset via {@link ChatSummary.workingDirectories | their own /// `workingDirectories`}; a chat that sets none operates against this full @@ -4497,9 +4497,6 @@ public struct Changeset: Codable, Sendable { /// /// - `'session'`: a static, session-wide changeset covering all changes the /// agent has produced in this session. - /// - `'directory'`: all changes the agent has produced within a single - /// {@link workingDirectory | working directory} of a multiroot session. - /// Paired with {@link workingDirectory}. /// - `'branch'`: changes relative to a base branch (e.g. a feature branch /// diffed against `main`). /// - `'uncommitted'`: the workspace's current uncommitted changes. @@ -4512,25 +4509,6 @@ public struct Changeset: Codable, Sendable { /// Implementations MAY provide additional values; clients SHOULD fall back /// to a reasonable default when an unknown value is encountered. public var changeKind: String - /// The working directory this changeset is scoped to. When set, it MUST be one - /// of the owning session's {@link SessionState.workingDirectories}, and every - /// file in the changeset belongs to that directory. - /// - /// **Grouping is the host's responsibility, not the client's.** A host whose - /// session has multiple working directories MUST group changes by directory — - /// emitting one changeset per working directory, each carrying its - /// `workingDirectory` — so that a client never has to derive a file's owning - /// directory itself (e.g. by prefix-matching URIs, which the client cannot do - /// correctly across nested repositories, symlinks, or submodules). This keeps - /// AHP a display-ready presentation model (see the - /// {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS - /// knowledge and hands clients pre-grouped changesets. - /// - /// Omit only for changesets that are genuinely not scoped to a single working - /// directory — e.g. a single-directory session (nothing to group), or an - /// aggregate roll-up / out-of-tree changeset that intentionally spans (or sits - /// outside) the working directories. - public var workingDirectory: String? /// Optional capability declarations for this changeset. Absent (or an empty /// object) means the changeset advertises no optional capabilities. /// @@ -4546,14 +4524,12 @@ public struct Changeset: Codable, Sendable { uriTemplate: String, description: String? = nil, changeKind: String, - workingDirectory: String? = nil, capabilities: ChangesetCapabilities? = nil ) { self.label = label self.uriTemplate = uriTemplate self.description = description self.changeKind = changeKind - self.workingDirectory = workingDirectory self.capabilities = capabilities } } diff --git a/docs/.changes/20260716-multiroot-sessions.json b/docs/.changes/20260716-multiroot-sessions.json index 6ef5062ae..d2ab878a9 100644 --- a/docs/.changes/20260716-multiroot-sessions.json +++ b/docs/.changes/20260716-multiroot-sessions.json @@ -1,5 +1,5 @@ { "type": "added", - "message": "Multiroot session support: `AgentCapabilities.multipleWorkspaceFolders` capability (with `immutablePrimary`), `CreateSessionParams.workingDirectories` and `SessionMetadata.workingDirectories` (equal-peer set, mirrored onto `SessionState`/`SessionSummary`), `CreateChatParams.workingDirectories` and `ChatState`/`ChatSummary.workingDirectories` (a subset of the session's set), the client-dispatchable `session/workingDirectorySet` / `session/workingDirectoryRemoved` and `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions for mutating those sets, and `Changeset.workingDirectory` (with a `'directory'` `changeKind`) so a multiroot host groups changes by directory.", + "message": "Multiroot session support: `AgentCapabilities.multipleWorkingDirectories` capability (with `immutablePrimary`), `CreateSessionParams.workingDirectories` and `SessionMetadata.workingDirectories` (equal-peer set, mirrored onto `SessionState`/`SessionSummary`), `CreateChatParams.workingDirectories` and `ChatState`/`ChatSummary.workingDirectories` (a subset of the session's set), and the client-dispatchable `session/workingDirectorySet` / `session/workingDirectoryRemoved` and `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions for mutating those sets.", "issues": [337] } diff --git a/docs/guide/changesets.md b/docs/guide/changesets.md index 05809bf2f..f2e8803f5 100644 --- a/docs/guide/changesets.md +++ b/docs/guide/changesets.md @@ -29,17 +29,10 @@ Changeset { uriTemplate: string description?: string /** - * Advisory hint: one of `'session'`, `'directory'`, `'branch'`, - * `'uncommitted'`, `'turn'`, or `'compare-turns'`. Other values allowed. + * Advisory hint: one of `'session'`, `'branch'`, `'uncommitted'`, + * `'turn'`, or `'compare-turns'`. Other values allowed. */ changeKind: string - /** - * Working directory this changeset is scoped to, when it covers a single - * directory of a multiroot session. One of the session's - * `workingDirectories`. Omitted for single-directory or session-wide - * changesets. - */ - workingDirectory?: URI /** Optional capability declarations (presence-flag objects). */ capabilities?: { /** Present ⇒ this changeset supports the per-file review workflow. */ @@ -64,11 +57,17 @@ unknown variables. ### Multiroot Sessions -Grouping changes by directory is the **host's** responsibility, not the client's. A host whose session has multiple [working directories](/guide/state-model#multiroot-sessions) **MUST** group its changes by directory — advertising one catalogue entry per working directory, each carrying a `workingDirectory` (one of the session's `workingDirectories`) and typically `changeKind: 'directory'`. Each such changeset is a flat list of files belonging to that one directory. - -This follows AHP's [doctrine](/guide/doctrine): the protocol is a display-ready presentation model in which the host owns the authoritative filesystem/VCS knowledge. A client MUST NOT be required to derive a file's owning directory itself (e.g. by prefix-matching URIs) — which it cannot do correctly across nested repositories, symlinks, or submodules. The host, which knows the real repository boundaries, hands clients pre-grouped changesets. - -`workingDirectory` is omitted only for changesets that are genuinely not scoped to one directory: a single-directory session, or an intentional aggregate roll-up / out-of-tree changeset. +A changeset is not scoped to a single working directory — a per-turn or +session-wide changeset naturally spans every directory the agent touched. A +client that wants to present changes grouped by directory does so itself, by +matching each file's URI against the session's +[`workingDirectories`](/guide/state-model#multiroot-sessions) (a list the +client already has); a client that does not care simply renders one tree. + +A host MAY *also* advertise dedicated per-directory changesets — one catalogue +entry per working directory — for clients that prefer server-scoped views. This +needs no extra field: the `changesets` catalogue is already a list, so a host +lists one entry per directory alongside the spanning ones. ### Changeset State diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index c02da1f5f..f0e5780b6 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -554,7 +554,7 @@ The forked session is an independent copy — subsequent changes to either sessi ## Multiroot Sessions A session can be granted tool access to more than one working directory when the -agent advertises the `multipleWorkspaceFolders` capability. All directories are +agent advertises the `multipleWorkingDirectories` capability. All directories are **equal peers** — there is no privileged "primary". ### Creating a multiroot session @@ -573,11 +573,11 @@ createSession({ ``` A client MUST NOT pass more than one entry unless the agent advertises -`multipleWorkspaceFolders`. Servers without that capability treat only the +`multipleWorkingDirectories`. Servers without that capability treat only the first entry as the session's working directory and ignore the rest. The directories are equal peers unless the agent advertises -`multipleWorkspaceFolders.immutablePrimary`, in which case the first entry is a +`multipleWorkingDirectories.immutablePrimary`, in which case the first entry is a fixed process root that clients MUST NOT remove or reorder. Forked sessions ignore `workingDirectories` — they inherit the working @@ -598,7 +598,7 @@ Both are `@clientDispatchable`. The resulting set is observed on result payload. Before dispatching either action, a client MUST verify that the agent advertises -`multipleWorkspaceFolders`. +`multipleWorkingDirectories`. ### Per-chat working-directory subsets @@ -640,7 +640,7 @@ subset: The subset is observed on `ChatState.workingDirectories`. A client MUST NOT dispatch these actions unless the agent advertises -`multipleWorkspaceFolders`. +`multipleWorkingDirectories`. ## Next Steps diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index ab6b58ecd..90529dcd6 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -51,12 +51,14 @@ Three roles, nested: one directory (or a few); when it pins nothing, it sees the session's whole set. -- **Changes are grouped *by directory*.** The file changes an agent produces are - surfaced as one changeset per directory, each carrying which directory it - belongs to — a flat list per root, not a nested tangle. +- **Changes are not locked to a directory.** A changeset (e.g. "last turn") may + span several working directories. Clients that want a per-directory view group + a changeset's files themselves against the session's directory list; a host + MAY additionally expose dedicated per-directory changesets as extra catalogue + entries. > One session, many equal directories. Each chat narrows to a subset. Changes -> group by directory. +> can span directories; grouping is optional. ```mermaid flowchart TB @@ -123,20 +125,18 @@ capabilities: directories (every entry must be one of the session's). A chat that narrows nothing operates against the whole set. -5. **Changes grouped by directory.** The session surfaces one changeset per - working directory, each tagged with the directory it covers — so a UI can show - "changes in repo-a / repo-b" without inventing arrays-of-arrays. - -6. **Server-owned change grouping.** A multiroot host MUST group changes by - directory — one changeset per working directory, each tagged with it — so - clients never re-derive a file's owning directory themselves. +5. **Optional per-directory change views.** Changesets are cross-cutting (a + per-turn changeset spans every directory the agent touched). A client that + wants a per-directory view groups a changeset's files itself against the + session's directory list; a host MAY also expose dedicated per-directory + changesets as extra catalogue entries. Nothing forces a one-directory scope. ```mermaid flowchart LR - Cap["capability:
multipleWorkspaceFolders?"] --> Create["createSession
workingDirectories[]"] + Cap["capability:
multipleWorkingDirectories?"] --> Create["createSession
workingDirectories[]"] Create --> Add["session/workingDirectorySet /
Removed actions"] Create --> Chat["chat subset
workingDirectories ⊆ session"] - Create --> CS["changesets
grouped by workingDirectory"] + Create --> CS["changesets
(optional per-dir entries)"] ``` --- @@ -152,7 +152,7 @@ kept as two separate checkouts. | Agent edits both repos | The agent has tool access to both directories, equal peers. | | User opens a focused thread on just the client | A chat pinned to `workingDirectories: [client]`. | | Agent later needs the shared `protos/` repo too | dispatch `session/workingDirectorySet(protos)` → set becomes `[api, client, protos]`. | -| User reviews the diff | Three changesets, one per directory, each tagged with its `workingDirectory`. | +| User reviews the diff | The session's changeset lists every changed file across the dirs; the client groups them by directory for display if it wants. | | The `protos/` work turns out unnecessary | dispatch `session/workingDirectoryRemoved(protos)` → set reconfigures back to `[api, client]`. | The session stays one coherent conversation and one shared configuration @@ -195,10 +195,11 @@ layer on without breaking this shape. (the session); a chat only ever *narrows*, never widens. This keeps the invariant simple: a chat's directories are always ⊆ the session's. -- **Changes group by directory, not arrays-of-arrays.** Reusing the existing - changeset catalogue — one entry per directory, tagged with its - `workingDirectory` — keeps each changeset a flat file list and avoids a new - nested shape. +- **Changesets stay cross-cutting.** A changeset isn't tied to one directory — + a per-turn diff can span several — so no directory field is forced onto it. + Per-directory presentation is optional: the client groups a changeset's files + against the known directory list, or the host advertises extra per-directory + catalogue entries. Both reuse existing shapes; neither needs arrays-of-arrays. - **Additive and capability-gated.** Every new field is optional; the commands are gated behind a capability plus a version handshake. A single-directory @@ -228,15 +229,14 @@ as today. | Symbol | File | Kind | | --- | --- | --- | -| `AgentCapabilities.multipleWorkspaceFolders?` | `channels-root/state.ts` | added (capability) | -| `MultipleWorkspaceFoldersCapability` (`immutablePrimary?`) | `channels-root/state.ts` | added (type) | +| `AgentCapabilities.multipleWorkingDirectories?` | `channels-root/state.ts` | added (capability) | +| `MultipleWorkingDirectoriesCapability` (`immutablePrimary?`) | `channels-root/state.ts` | added (type) | | `CreateSessionParams.workingDirectories?` | `channels-session/commands.ts` | added | | `SessionMetadata.workingDirectories?` (→ `SessionState`, `SessionSummary`) | `channels-session/state.ts` | added | | `session/workingDirectorySet` / `session/workingDirectoryRemoved` actions | `channels-session/actions.ts` | added (action) | | `ChatState.workingDirectories?` / `ChatSummary.workingDirectories?` | `channels-chat/state.ts` | added | | `CreateChatParams.workingDirectories?` | `channels-chat/commands.ts` | added | | `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions | `channels-chat/actions.ts` | added (action) | -| `Changeset.workingDirectory?` + `'directory'` `changeKind` | `channels-changeset/state.ts` | added | | 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.6.0`) | `common/actions.ts`, `version/registry.ts` | added | > **Revised after review.** The directory-mutation surface started as four @@ -245,7 +245,12 @@ as today. > following the keyed-collection convention — the set lives in state, so clients > mutate it by dispatching actions and observe the result on > `workingDirectories`. The deprecated singular `workingDirectory` fields were -> **hard-removed** (0.6.0 is a breaking release), not kept as a shorthand. +> **hard-removed** (0.6.0 is a breaking release), not kept as a shorthand. An +> earlier revision also added a `Changeset.workingDirectory` field to group +> changes per directory; that was **dropped** — a changeset can span +> directories (e.g. a per-turn diff), so a single-directory scalar is the wrong +> model. Per-directory presentation is instead handled by optional client-side +> grouping or extra per-directory catalogue entries (see §6/§7). ### 8.2 Type signatures @@ -254,9 +259,9 @@ as today. interface AgentCapabilities { // …existing… /** Presence ({}) = the agent supports >1 working directory per session. */ - multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability; + multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability; } -interface MultipleWorkspaceFoldersCapability { +interface MultipleWorkingDirectoriesCapability { /** First directory is a fixed process root; clients MUST NOT remove/reorder it. */ immutablePrimary?: boolean; } @@ -275,7 +280,7 @@ interface SessionMetadata { } // ── Session runtime mutation — channels-session/actions.ts ──────────────── -// channel = session URI. Gated by multipleWorkspaceFolders. @clientDispatchable. +// channel = session URI. Gated by multipleWorkingDirectories. @clientDispatchable. interface SessionWorkingDirectorySetAction { type: ActionType.SessionWorkingDirectorySet; // 'session/workingDirectorySet' directory: URI; // appended; no-op if present @@ -295,7 +300,7 @@ interface CreateChatParams extends BaseParams { // …existing… workingDirectories?: URI[]; // subset ⊆ session; absent → whole session set } -// channel = chat URI. Gated by multipleWorkspaceFolders. @clientDispatchable. +// channel = chat URI. Gated by multipleWorkingDirectories. @clientDispatchable. interface ChatWorkingDirectorySetAction { type: ActionType.ChatWorkingDirectorySet; // 'chat/workingDirectorySet' directory: URI; // MUST be in the session set @@ -306,16 +311,10 @@ interface ChatWorkingDirectoryRemovedAction { } // ── Changes — channels-changeset/state.ts ──────────────────────────────── -interface Changeset { - // …existing… - changeKind: string; // now also accepts 'directory' - /** - * Directory this changeset is scoped to (one of the session's dirs). - * A multiroot host MUST group changes by directory (one changeset per dir); - * omitted only for single-dir sessions or out-of-tree/aggregate changesets. - */ - workingDirectory?: URI; -} +// No changes. A changeset can span working directories, so it carries no +// directory field. Per-directory views are optional: clients group a +// changeset's files against the session's workingDirectories, or a host +// advertises extra per-directory catalogue entries. ``` ### 8.3 Versioning & gating @@ -325,9 +324,9 @@ interface Changeset { = `[0.6.0, 0.5.2, 0.5.1]`. - The four directory mutations are **state actions**, so they carry `ACTION_INTRODUCED_IN` entries at `0.6.0` (and are `@clientDispatchable`). - Everything else — the capability, the create-time fields, and the - `Changeset.workingDirectory` field — is gated by the `multipleWorkspaceFolders` - capability plus the `initialize` version handshake. + Everything else — the capability and the create-time / state fields — is gated + by the `multipleWorkingDirectories` capability plus the `initialize` version + handshake. - Removal actions are idempotent and modelled as *reconfigure-to-the-reduced-set*; a host MAY decline to apply a removal (e.g. an `immutablePrimary` directory), leaving the set unchanged. @@ -340,7 +339,7 @@ interface Changeset { "primary + additional" split because it re-introduces the confusion the user called out ("what's the relation between primary and secondary?"). Backends that genuinely pin a fixed process root opt in via - `MultipleWorkspaceFoldersCapability.immutablePrimary` (index `0` fixed). + `MultipleWorkingDirectoriesCapability.immutablePrimary` (index `0` fixed). - **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* 0.6.0 is a breaking release, so the deprecated singular fields on @@ -359,13 +358,17 @@ interface Changeset { the session's set," with absent meaning the whole set. Gated by the same capability. -- **Changes grouped by directory — server's job.** *Resolved (per review - pushback):* a multiroot host MUST group changes by directory - (`Changeset.workingDirectory`, one changeset per dir). Grouping is a host - responsibility per AHP's doctrine (display-ready state; host owns the - filesystem/VCS model) — clients must not re-derive it from file URIs, which - they cannot do correctly across nested repos/symlinks. The field stays - optional only for single-dir sessions and out-of-tree/aggregate changesets. +- **No directory field on changesets.** *Resolved (per review):* an earlier + revision put a `Changeset.workingDirectory` scalar on the changeset and made + a multiroot host MUST group by directory. Dropped — a changeset can span + directories (a per-turn diff touches several), so a single-directory scalar + can't model the common case, and reviewers noted matching a file to a + directory is a simple client-side operation against the known directory list + (not a VCS-boundary problem). Per-directory presentation is therefore + optional: clients group a changeset's files themselves, and a host MAY expose + extra per-directory catalogue entries (already possible — the `changesets` + list is unbounded). A machine-readable per-directory association (e.g. a + `{workingDirectory}` template variable) is deferred to a follow-up if needed. - **Removal semantics.** *Resolved:* no single-remove primitive is assumed; the `*/workingDirectoryRemoved` action reduces to the reduced set, so it is @@ -395,7 +398,7 @@ interface Changeset { - **Before:** a session is scoped to one `workingDirectory`. - **After:** a session owns a **set of equal-peer directories**; a **chat** works - in a **subset**; **changes group by directory** (server-side). + in a **subset**; **changesets can span directories** (grouping is optional). - **Why:** cross-directory / multi-repo / multi-root-workspace tasks are one coherent piece of work, not N disconnected sessions. - **How it stays safe:** capability-gated, additive fields; directory mutations diff --git a/schema/actions.schema.json b/schema/actions.schema.json index bd1286283..9d488957a 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -410,7 +410,7 @@ }, "SessionWorkingDirectorySetAction": { "type": "object", - "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkspaceFolders}.", + "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}.", "properties": { "type": { "const": "session/workingDirectorySet" @@ -427,7 +427,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves\nthe set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" @@ -1224,7 +1224,7 @@ }, "ChatWorkingDirectorySetAction": { "type": "object", - "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}.", + "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.", "properties": { "type": { "const": "chat/workingDirectorySet" @@ -2646,9 +2646,9 @@ "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." }, - "multipleWorkspaceFolders": { - "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "multipleWorkingDirectories": { + "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -2662,9 +2662,9 @@ } } }, - "MultipleWorkspaceFoldersCapability": { + "MultipleWorkingDirectoriesCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { "immutablePrimary": { "type": "boolean", @@ -2791,7 +2791,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2833,7 +2833,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3105,7 +3105,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -6385,11 +6385,7 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 86ca15fa0..c4acbc09b 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -960,7 +960,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -1132,7 +1132,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}." + "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." } }, "required": [ @@ -1847,9 +1847,9 @@ "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." }, - "multipleWorkspaceFolders": { - "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "multipleWorkingDirectories": { + "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -1863,9 +1863,9 @@ } } }, - "MultipleWorkspaceFoldersCapability": { + "MultipleWorkingDirectoriesCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { "immutablePrimary": { "type": "boolean", @@ -1992,7 +1992,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2034,7 +2034,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2306,7 +2306,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -5586,11 +5586,7 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", @@ -6314,7 +6310,7 @@ }, "SessionWorkingDirectorySetAction": { "type": "object", - "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkspaceFolders}.", + "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}.", "properties": { "type": { "const": "session/workingDirectorySet" @@ -6331,7 +6327,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves\nthe set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" @@ -7128,7 +7124,7 @@ }, "ChatWorkingDirectorySetAction": { "type": "object", - "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}.", + "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.", "properties": { "type": { "const": "chat/workingDirectorySet" diff --git a/schema/errors.schema.json b/schema/errors.schema.json index c9639921a..1b46020fa 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -628,9 +628,9 @@ "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." }, - "multipleWorkspaceFolders": { - "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "multipleWorkingDirectories": { + "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -644,9 +644,9 @@ } } }, - "MultipleWorkspaceFoldersCapability": { + "MultipleWorkingDirectoriesCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { "immutablePrimary": { "type": "boolean", @@ -773,7 +773,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -815,7 +815,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1087,7 +1087,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -4367,11 +4367,7 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", @@ -5645,7 +5641,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -5817,7 +5813,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}." + "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." } }, "required": [ @@ -7189,7 +7185,7 @@ }, "SessionWorkingDirectorySetAction": { "type": "object", - "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkspaceFolders}.", + "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}.", "properties": { "type": { "const": "session/workingDirectorySet" @@ -7206,7 +7202,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves\nthe set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" @@ -7963,7 +7959,7 @@ }, "ChatWorkingDirectorySetAction": { "type": "object", - "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkspaceFolders}.", + "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.", "properties": { "type": { "const": "chat/workingDirectorySet" diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index ff6f444fa..0f269c319 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -103,7 +103,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -791,9 +791,9 @@ "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." }, - "multipleWorkspaceFolders": { - "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "multipleWorkingDirectories": { + "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -807,9 +807,9 @@ } } }, - "MultipleWorkspaceFoldersCapability": { + "MultipleWorkingDirectoriesCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { "immutablePrimary": { "type": "boolean", @@ -936,7 +936,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -978,7 +978,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1250,7 +1250,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -4530,11 +4530,7 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/schema/state.schema.json b/schema/state.schema.json index f71ef3232..f3098e7b1 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -539,9 +539,9 @@ "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nforking; set {@link MultipleChatsCapability.fork} to also allow forking." }, - "multipleWorkspaceFolders": { - "$ref": "#/$defs/MultipleWorkspaceFoldersCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "multipleWorkingDirectories": { + "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -555,9 +555,9 @@ } } }, - "MultipleWorkspaceFoldersCapability": { + "MultipleWorkingDirectoriesCapability": { "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability.", + "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { "immutablePrimary": { "type": "boolean", @@ -684,7 +684,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -726,7 +726,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -998,7 +998,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -4278,11 +4278,7 @@ }, "changeKind": { "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'directory'`: all changes the agent has produced within a single\n {@link workingDirectory | working directory} of a multiroot session.\n Paired with {@link workingDirectory}.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "The working directory this changeset is scoped to. When set, it MUST be one\nof the owning session's {@link SessionState.workingDirectories}, and every\nfile in the changeset belongs to that directory.\n\n**Grouping is the host's responsibility, not the client's.** A host whose\nsession has multiple working directories MUST group changes by directory —\nemitting one changeset per working directory, each carrying its\n`workingDirectory` — so that a client never has to derive a file's owning\ndirectory itself (e.g. by prefix-matching URIs, which the client cannot do\ncorrectly across nested repositories, symlinks, or submodules). This keeps\nAHP a display-ready presentation model (see the\n{@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS\nknowledge and hands clients pre-grouped changesets.\n\nOmit only for changesets that are genuinely not scoped to a single working\ndirectory — e.g. a single-directory session (nothing to group), or an\naggregate roll-up / out-of-tree changeset that intentionally spans (or sits\noutside) the working directories." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, "capabilities": { "$ref": "#/$defs/ChangesetCapabilities", diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 6a03a9119..d164bf2a0 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -659,7 +659,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'AgentInfo' }, { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, - { name: 'MultipleWorkspaceFoldersCapability' }, + { name: 'MultipleWorkingDirectoriesCapability' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index ea28256a4..9a5408017 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -802,7 +802,7 @@ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'AgentCapabilities', 'MultipleChatsCapability', - 'MultipleWorkspaceFoldersCapability', + 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 04da0303d..980b3c969 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -690,7 +690,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'AgentInfo' }, { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, - { name: 'MultipleWorkspaceFoldersCapability' }, + { name: 'MultipleWorkingDirectoriesCapability' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 062ea0210..a27ce8def 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -552,7 +552,7 @@ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'AgentCapabilities', 'MultipleChatsCapability', - 'MultipleWorkspaceFoldersCapability', + 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', diff --git a/types/channels-changeset/state.ts b/types/channels-changeset/state.ts index 6adc4bfed..58bd16d80 100644 --- a/types/channels-changeset/state.ts +++ b/types/channels-changeset/state.ts @@ -5,7 +5,7 @@ * @module channels-changeset/state */ -import type { URI, StringOrMarkdown, FileEdit, ErrorInfo } from '../common/state.js'; +import type { StringOrMarkdown, FileEdit, ErrorInfo } from '../common/state.js'; // ─── Changesets ────────────────────────────────────────────────────────────── @@ -50,9 +50,6 @@ export interface Changeset { * * - `'session'`: a static, session-wide changeset covering all changes the * agent has produced in this session. - * - `'directory'`: all changes the agent has produced within a single - * {@link workingDirectory | working directory} of a multiroot session. - * Paired with {@link workingDirectory}. * - `'branch'`: changes relative to a base branch (e.g. a feature branch * diffed against `main`). * - `'uncommitted'`: the workspace's current uncommitted changes. @@ -66,27 +63,6 @@ export interface Changeset { * to a reasonable default when an unknown value is encountered. */ changeKind: string; - /** - * The working directory this changeset is scoped to. When set, it MUST be one - * of the owning session's {@link SessionState.workingDirectories}, and every - * file in the changeset belongs to that directory. - * - * **Grouping is the host's responsibility, not the client's.** A host whose - * session has multiple working directories MUST group changes by directory — - * emitting one changeset per working directory, each carrying its - * `workingDirectory` — so that a client never has to derive a file's owning - * directory itself (e.g. by prefix-matching URIs, which the client cannot do - * correctly across nested repositories, symlinks, or submodules). This keeps - * AHP a display-ready presentation model (see the - * {@link /guide/doctrine | doctrine}): the host owns the filesystem/VCS - * knowledge and hands clients pre-grouped changesets. - * - * Omit only for changesets that are genuinely not scoped to a single working - * directory — e.g. a single-directory session (nothing to group), or an - * aggregate roll-up / out-of-tree changeset that intentionally spans (or sits - * outside) the working directories. - */ - workingDirectory?: URI; /** * Optional capability declarations for this changeset. Absent (or an empty * object) means the changeset advertises no optional capabilities. diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index b642c9af9..c9b946bab 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -452,7 +452,7 @@ export interface ChatActivityChangedAction { * subset if absent) and is a no-op when it is already present. `directory` MUST * be one of the owning session's {@link SessionState.workingDirectories}; a host * MUST reject a directory that is not. Only valid when the agent advertises - * {@link AgentCapabilities.multipleWorkspaceFolders}. + * {@link AgentCapabilities.multipleWorkingDirectories}. * * @category Chat Actions * @version 1 diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 745df0bde..377f4df3c 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -46,7 +46,7 @@ export interface CreateChatParams extends BaseParams { * `workingDirectories`; this field is ignored for forked chats. * * A client MUST NOT supply this field unless the agent advertises - * {@link AgentCapabilities.multipleWorkspaceFolders}. + * {@link AgentCapabilities.multipleWorkingDirectories}. */ workingDirectories?: URI[]; } diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index cd329547a..49d71ee84 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -113,14 +113,14 @@ export interface AgentCapabilities { /** * The session's agent can be granted tool access to more than one working * directory. The directories are treated as equal peers except where the - * agent advertises {@link MultipleWorkspaceFoldersCapability.immutablePrimary} + * agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} * (some backends pin their first directory as a fixed process root). * * When absent, clients MUST NOT mutate a session's or chat's working-directory * set and MUST NOT set more than one entry in * {@link CreateSessionParams.workingDirectories}. */ - multipleWorkspaceFolders?: MultipleWorkspaceFoldersCapability; + multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability; } /** @@ -138,11 +138,11 @@ export interface MultipleChatsCapability { } /** - * Options for the {@link AgentCapabilities.multipleWorkspaceFolders} capability. + * Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. * * @category Root State */ -export interface MultipleWorkspaceFoldersCapability { +export interface MultipleWorkingDirectoriesCapability { /** * The agent's **first** working directory (index `0` of * {@link CreateSessionParams.workingDirectories}) is an immutable primary: diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index d40240b46..5ba4ef058 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -264,7 +264,7 @@ export interface SessionActiveClientRemovedAction { * Membership semantics keyed by the directory URI: the reducer appends * `directory` when the set does not already contain it (creating the set if * absent) and is a no-op when it is already present. Only valid when the agent - * advertises {@link AgentCapabilities.multipleWorkspaceFolders}. + * advertises {@link AgentCapabilities.multipleWorkingDirectories}. * * @category Session Actions * @version 1 @@ -284,7 +284,7 @@ export interface SessionWorkingDirectorySetAction { * atomic backend "remove one" primitive — a host reconfigures its agent to the * reduced set — so this action is safe to model as idempotent. A host MAY * decline to apply the removal (e.g. an immutable primary directory, see - * {@link MultipleWorkspaceFoldersCapability.immutablePrimary}); it then leaves + * {@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves * the set unchanged. * * @category Session Actions diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index 6acf1fc94..7b07a61ae 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -70,11 +70,11 @@ export interface CreateSessionParams extends BaseParams { * The working directories the session's agent is granted tool access to. * A session may span multiple directories; they are equal peers except when * the agent advertises - * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (in which case + * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case * the first entry is a fixed process root). * * A client MUST NOT supply more than one entry unless the agent advertises - * {@link AgentCapabilities.multipleWorkspaceFolders}; a server without that + * {@link AgentCapabilities.multipleWorkingDirectories}; a server without that * capability treats only the first entry as the session's working directory * and ignores the rest. Dispatch `session/workingDirectorySet` / * `session/workingDirectoryRemoved` to change the set after the session has diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index aa7316330..c71061ce0 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -87,7 +87,7 @@ export interface SessionMetadata { * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkspaceFoldersCapability.immutablePrimary} (the first + * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first * entry is then a fixed process root). Individual chats MAY restrict to a * subset via {@link ChatSummary.workingDirectories | their own * `workingDirectories`}; a chat that sets none operates against this full From 11f1a65eb03dc93ac5ae78bdbd4b9430e37965d8 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sat, 18 Jul 2026 07:40:32 -0700 Subject: [PATCH 09/15] fix(clients): implement working-directory actions in native reducers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI (cargo/go/gradle/swift test) went red after the merge because the four new working-directory state actions were only handled by the TypeScript reducer — the hand-written Rust/Go/Kotlin/Swift reducers no-op'd them, so the shared conformance fixtures (append/remove cases) failed. Add the session/chat workingDirectorySet + workingDirectoryRemoved cases to each native reducer (idempotent set / no-op remove, mirroring activeClientSet). Also fix two Rust integration-test struct literals that still set the removed singular `working_directory` field (cargo test compiles tests; the earlier local `cargo build` did not, so it slipped through). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahp/reducers.go | 32 ++++++++++++++ .../microsoft/agenthostprotocol/Reducers.kt | 42 +++++++++++++++++++ clients/rust/crates/ahp/src/reducers.rs | 36 ++++++++++++++++ clients/rust/crates/ahp/tests/hosts.rs | 2 +- .../ahp/tests/multi_host_state_mirror.rs | 4 +- .../Sources/AgentHostProtocol/Reducers.swift | 24 +++++++++++ 6 files changed, 137 insertions(+), 3 deletions(-) diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 7cedf39e8..43f659a4b 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -514,6 +514,22 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R case *ahptypes.ChatActivityChangedAction: state.Activity = a.Activity return ReduceOutcomeApplied + case *ahptypes.ChatWorkingDirectorySetAction: + for _, d := range state.WorkingDirectories { + if d == a.Directory { + return ReduceOutcomeNoOp + } + } + state.WorkingDirectories = append(state.WorkingDirectories, a.Directory) + return ReduceOutcomeApplied + case *ahptypes.ChatWorkingDirectoryRemovedAction: + for i := range state.WorkingDirectories { + if state.WorkingDirectories[i] == a.Directory { + state.WorkingDirectories = append(state.WorkingDirectories[:i], state.WorkingDirectories[i+1:]...) + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp case *ahptypes.ChatToolCallStartAction: if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId { return ReduceOutcomeNoOp @@ -858,6 +874,22 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct } } return ReduceOutcomeNoOp + case *ahptypes.SessionWorkingDirectorySetAction: + for _, d := range state.WorkingDirectories { + if d == a.Directory { + return ReduceOutcomeNoOp + } + } + state.WorkingDirectories = append(state.WorkingDirectories, a.Directory) + return ReduceOutcomeApplied + case *ahptypes.SessionWorkingDirectoryRemovedAction: + for i := range state.WorkingDirectories { + if state.WorkingDirectories[i] == a.Directory { + state.WorkingDirectories = append(state.WorkingDirectories[:i], state.WorkingDirectories[i+1:]...) + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp case *ahptypes.SessionInputNeededSetAction: id, ok := sessionInputRequestID(a.Request) if !ok { diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index c5f35618f..e681f91fb 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -605,6 +605,27 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } + is StateActionSessionWorkingDirectorySet -> { + val list = state.workingDirectories ?: emptyList() + if (list.contains(action.value.directory)) { + state + } else { + state.copy(workingDirectories = list + action.value.directory) + } + } + + is StateActionSessionWorkingDirectoryRemoved -> { + val list = state.workingDirectories + val idx = list?.indexOf(action.value.directory) ?: -1 + if (list == null || idx < 0) { + state + } else { + val updated = list.toMutableList() + updated.removeAt(idx) + state.copy(workingDirectories = updated) + } + } + is StateActionSessionInputNeededSet -> { val request = action.value.request val id = sessionInputRequestId(request) @@ -843,6 +864,27 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when is StateActionChatActivityChanged -> state.copy(activity = action.value.activity) + is StateActionChatWorkingDirectorySet -> { + val list = state.workingDirectories ?: emptyList() + if (list.contains(action.value.directory)) { + state + } else { + state.copy(workingDirectories = list + action.value.directory) + } + } + + is StateActionChatWorkingDirectoryRemoved -> { + val list = state.workingDirectories + val idx = list?.indexOf(action.value.directory) ?: -1 + if (list == null || idx < 0) { + state + } else { + val updated = list.toMutableList() + updated.removeAt(idx) + state.copy(workingDirectories = updated) + } + } + // ── Tool Call State Machine ─────────────────────────────────────────── is StateActionChatToolCallStart -> { diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 9a60e2ec3..b5cbd1884 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -740,6 +740,24 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - state.active_clients.remove(idx); ReduceOutcome::Applied } + StateAction::SessionWorkingDirectorySet(a) => { + let list = state.working_directories.get_or_insert_with(Vec::new); + if list.contains(&a.directory) { + return ReduceOutcome::NoOp; + } + list.push(a.directory.clone()); + ReduceOutcome::Applied + } + StateAction::SessionWorkingDirectoryRemoved(a) => { + let Some(list) = state.working_directories.as_mut() else { + return ReduceOutcome::NoOp; + }; + let Some(idx) = list.iter().position(|d| *d == a.directory) else { + return ReduceOutcome::NoOp; + }; + list.remove(idx); + ReduceOutcome::Applied + } StateAction::SessionInputNeededSet(a) => { let Some(action_id) = session_input_request_id(&a.request) else { return ReduceOutcome::NoOp; @@ -947,6 +965,24 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu state.activity = a.activity.clone(); ReduceOutcome::Applied } + StateAction::ChatWorkingDirectorySet(a) => { + let list = state.working_directories.get_or_insert_with(Vec::new); + if list.contains(&a.directory) { + return ReduceOutcome::NoOp; + } + list.push(a.directory.clone()); + ReduceOutcome::Applied + } + StateAction::ChatWorkingDirectoryRemoved(a) => { + let Some(list) = state.working_directories.as_mut() else { + return ReduceOutcome::NoOp; + }; + let Some(idx) = list.iter().position(|d| *d == a.directory) else { + return ReduceOutcome::NoOp; + }; + list.remove(idx); + ReduceOutcome::Applied + } StateAction::ChatToolCallStart(a) => { let Some(active) = state.active_turn.as_mut() else { return ReduceOutcome::NoOp; diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs index 2e1f4b06f..9031a7658 100644 --- a/clients/rust/crates/ahp/tests/hosts.rs +++ b/clients/rust/crates/ahp/tests/hosts.rs @@ -1039,7 +1039,7 @@ fn make_summary(uri: &str, title: &str, modified_at: i64) -> ahp_types::state::S created_at: "1970-01-01T00:00:00.000Z".into(), modified_at: modified, project: None, - working_directory: None, + working_directories: None, changes: None, annotations: None, meta: None, diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index 5f8ad2dfe..23969f940 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -51,7 +51,7 @@ fn session_state(title: &str, _resource: &str) -> SessionState { status: SessionStatus::Idle.bits(), activity: None, project: None, - working_directory: None, + working_directories: None, annotations: None, lifecycle: SessionLifecycle::Ready, creation_error: None, @@ -354,7 +354,7 @@ fn non_action_event_is_ignored() { created_at: "1970-01-01T00:00:00.000Z".into(), modified_at: "1970-01-01T00:00:00.000Z".into(), project: None, - working_directory: None, + working_directories: None, changes: None, annotations: None, meta: None, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 9f7725009..dc7027fcd 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -153,6 +153,18 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { next.activity = a.activity return next + case .chatWorkingDirectorySet(let a): + if (state.workingDirectories ?? []).contains(a.directory) { return state } + var next = state + next.workingDirectories = (next.workingDirectories ?? []) + [a.directory] + return next + + case .chatWorkingDirectoryRemoved(let a): + guard let idx = state.workingDirectories?.firstIndex(of: a.directory) else { return state } + var next = state + next.workingDirectories?.remove(at: idx) + return next + // ── Tool Call State Machine ─────────────────────────────────────────── case .chatToolCallStart(let a): @@ -723,6 +735,18 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.activeClients.remove(at: idx) return next + case .sessionWorkingDirectorySet(let a): + if (state.workingDirectories ?? []).contains(a.directory) { return state } + var next = state + next.workingDirectories = (next.workingDirectories ?? []) + [a.directory] + return next + + case .sessionWorkingDirectoryRemoved(let a): + guard let idx = state.workingDirectories?.firstIndex(of: a.directory) else { return state } + var next = state + next.workingDirectories?.remove(at: idx) + return next + case .sessionInputNeededSet(let a): guard let id = sessionInputRequestID(a.request) else { return state } var next = state From 40eb4888771db664227f2d5ec8bb2902328684c1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 21 Jul 2026 14:24:25 -0700 Subject: [PATCH 10/15] spec: target 0.7.0 for multiroot (0.6.0 shipped without it) spec/v0.6.0 released on 2026-07-20 without multiroot and still contains the singular `workingDirectory` fields this feature removes. Removing released state fields breaks compatibility within the 0.6.x MINOR, so per the versioning policy this feature must land in a new MINOR. - Bump PROTOCOL_VERSION 0.6.1 -> 0.7.0; SUPPORTED_PROTOCOL_VERSIONS = [0.7.0, 0.6.0, 0.5.2, 0.5.1]. - Retag the four working-directory actions ACTION_INTRODUCED_IN 0.6.0 -> 0.7.0 (0.6.0 shipped without them, so a 0.6.0 peer does not understand them). - Rotate the CHANGELOG dev heading 0.6.1 -> 0.7.0 and update the proposal's versioning section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- clients/go/ahptypes/version.generated.go | 4 ++-- clients/go/release-metadata.json | 2 +- clients/kotlin/release-metadata.json | 2 +- .../generated/Version.generated.kt | 4 ++-- clients/rust/crates/ahp-types/src/version.rs | 4 ++-- clients/rust/release-metadata.json | 2 +- .../Generated/Version.generated.swift | 4 ++-- clients/swift/release-metadata.json | 2 +- clients/typescript/release-metadata.json | 2 +- docs/proposals/multiroot-sessions.md | 23 +++++++++++-------- types/version/registry.ts | 12 +++++----- 12 files changed, 34 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cde81419..f2d98284c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,9 @@ changes accumulate. Track in-flight protocol changes via PRs touching `NOTIFICATION_INTRODUCED_IN` maps in [`types/version/registry.ts`](types/version/registry.ts). -## [0.6.1] — Unreleased +## [0.7.0] — Unreleased -Spec version: `0.6.1` +Spec version: `0.7.0` ## [0.6.0] — 2026-07-20 diff --git a/clients/go/ahptypes/version.generated.go b/clients/go/ahptypes/version.generated.go index 26d70813d..058869903 100644 --- a/clients/go/ahptypes/version.generated.go +++ b/clients/go/ahptypes/version.generated.go @@ -6,13 +6,13 @@ package ahptypes // ProtocolVersion is the current protocol version (SemVer // MAJOR.MINOR.PATCH) that this generated source speaks. -const ProtocolVersion = "0.6.1" +const ProtocolVersion = "0.7.0" // supportedProtocolVersions backs [SupportedProtocolVersions] — held // in an unexported slice so callers cannot accidentally mutate the // shared backing array. var supportedProtocolVersions = []string{ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/go/release-metadata.json b/clients/go/release-metadata.json index cb60446f6..f26219c29 100644 --- a/clients/go/release-metadata.json +++ b/clients/go/release-metadata.json @@ -2,7 +2,7 @@ "client": "go", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/kotlin/release-metadata.json b/clients/kotlin/release-metadata.json index 251a45a6b..c6669d28d 100644 --- a/clients/kotlin/release-metadata.json +++ b/clients/kotlin/release-metadata.json @@ -2,7 +2,7 @@ "client": "kotlin", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt index 97bcc076b..bab670ef8 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt @@ -5,7 +5,7 @@ package com.microsoft.agenthostprotocol.generated /** * Current protocol version (SemVer `MAJOR.MINOR.PATCH`). */ -public const val PROTOCOL_VERSION: String = "0.6.1" +public const val PROTOCOL_VERSION: String = "0.7.0" /** * Every protocol version this library is willing to negotiate, ordered @@ -16,7 +16,7 @@ public const val PROTOCOL_VERSION: String = "0.6.1" * protocol versions if the host doesn't accept the newest one. */ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/rust/crates/ahp-types/src/version.rs b/clients/rust/crates/ahp-types/src/version.rs index edc46573f..d05243be0 100644 --- a/clients/rust/crates/ahp-types/src/version.rs +++ b/clients/rust/crates/ahp-types/src/version.rs @@ -5,7 +5,7 @@ #![allow(missing_docs)] /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -pub const PROTOCOL_VERSION: &str = "0.6.1"; +pub const PROTOCOL_VERSION: &str = "0.7.0"; /// Every protocol version this crate is willing to negotiate, ordered /// most-preferred-first. The first entry equals [`PROTOCOL_VERSION`]. @@ -13,4 +13,4 @@ pub const PROTOCOL_VERSION: &str = "0.6.1"; /// Consumers building `InitializeParams` should pass this slice (or a /// derived `Vec`) so the same client binary can fall back to /// older protocol versions if the host doesn't accept the newest one. -pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.6.1", "0.6.0", "0.5.2", "0.5.1"]; +pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.7.0", "0.6.0", "0.5.2", "0.5.1"]; diff --git a/clients/rust/release-metadata.json b/clients/rust/release-metadata.json index 3744ab4a4..1fba6f631 100644 --- a/clients/rust/release-metadata.json +++ b/clients/rust/release-metadata.json @@ -2,7 +2,7 @@ "client": "rust", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift index 902600cd6..e925c5866 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift @@ -3,7 +3,7 @@ import Foundation /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -public let PROTOCOL_VERSION: String = "0.6.1" +public let PROTOCOL_VERSION: String = "0.7.0" /// Every protocol version this package is willing to negotiate, /// ordered most-preferred-first. The first entry equals @@ -13,7 +13,7 @@ public let PROTOCOL_VERSION: String = "0.6.1" /// `InitializeParams` so the same client binary can fall back to older /// protocol versions if the host doesn't accept the newest one. public let SUPPORTED_PROTOCOL_VERSIONS: [String] = [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/swift/release-metadata.json b/clients/swift/release-metadata.json index f83f6cee9..7aa7042b2 100644 --- a/clients/swift/release-metadata.json +++ b/clients/swift/release-metadata.json @@ -2,7 +2,7 @@ "client": "swift", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/typescript/release-metadata.json b/clients/typescript/release-metadata.json index f53bcb54f..37c6ac0b8 100644 --- a/clients/typescript/release-metadata.json +++ b/clients/typescript/release-metadata.json @@ -2,7 +2,7 @@ "client": "typescript", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index 90529dcd6..f89f6afd0 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -219,7 +219,7 @@ flowchart LR The concrete, additive changes that back the framing above — a reviewer's map of the `types/` surface. Full prose detail lives in the [State Model](/guide/state-model#multiroot-sessions) and -[Changesets](/guide/changesets) guides. Target spec version: **0.6.0**. +[Changesets](/guide/changesets) guides. Target spec version: **0.7.0**. Everything here is **additive and optional** — no field is required, no existing field changes type, and old clients that ignore the new surface behave exactly @@ -237,7 +237,7 @@ as today. | `ChatState.workingDirectories?` / `ChatSummary.workingDirectories?` | `channels-chat/state.ts` | added | | `CreateChatParams.workingDirectories?` | `channels-chat/commands.ts` | added | | `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions | `channels-chat/actions.ts` | added (action) | -| 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.6.0`) | `common/actions.ts`, `version/registry.ts` | added | +| 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.7.0`) | `common/actions.ts`, `version/registry.ts` | added | > **Revised after review.** The directory-mutation surface started as four > **commands** (`add/removeWorkspaceFolder`, `add/removeChatWorkspaceFolder` @@ -245,7 +245,7 @@ as today. > following the keyed-collection convention — the set lives in state, so clients > mutate it by dispatching actions and observe the result on > `workingDirectories`. The deprecated singular `workingDirectory` fields were -> **hard-removed** (0.6.0 is a breaking release), not kept as a shorthand. An +> **hard-removed** (a breaking change → 0.7.0), not kept as a shorthand. An > earlier revision also added a `Changeset.workingDirectory` field to group > changes per directory; that was **dropped** — a changeset can span > directories (e.g. a per-turn diff), so a single-directory scalar is the wrong @@ -319,11 +319,12 @@ interface ChatWorkingDirectoryRemovedAction { ### 8.3 Versioning & gating -- `PROTOCOL_VERSION` is **`0.6.0`** (a breaking release; the branch's base - already advanced the ongoing-dev version to `0.6.0`). `SUPPORTED_PROTOCOL_VERSIONS` - = `[0.6.0, 0.5.2, 0.5.1]`. +- `PROTOCOL_VERSION` is **`0.7.0`** — a MINOR bump because the feature is + breaking (it removes the singular `workingDirectory` fields that shipped in the + released `0.6.0`), and pre-1.0 breaking changes land in a MINOR. + `SUPPORTED_PROTOCOL_VERSIONS` = `[0.7.0, 0.6.0, 0.5.2, 0.5.1]`. - The four directory mutations are **state actions**, so they carry - `ACTION_INTRODUCED_IN` entries at `0.6.0` (and are `@clientDispatchable`). + `ACTION_INTRODUCED_IN` entries at `0.7.0` (and are `@clientDispatchable`). Everything else — the capability and the create-time / state fields — is gated by the `multipleWorkingDirectories` capability plus the `initialize` version handshake. @@ -341,10 +342,12 @@ interface ChatWorkingDirectoryRemovedAction { that genuinely pin a fixed process root opt in via `MultipleWorkingDirectoriesCapability.immutablePrimary` (index `0` fixed). -- **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* 0.6.0 - is a breaking release, so the deprecated singular fields on +- **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* + removing these fields is a breaking change, so the feature targets a MINOR + bump (`0.7.0`); the deprecated singular fields on `CreateSessionParams` / `SessionMetadata` / `ChatState` / `ChatSummary` are - removed outright rather than kept as a shorthand. + removed outright rather than kept as a shorthand. (Originally planned to ride + `0.6.0`'s breaking window, but `0.6.0` shipped without this feature.) - **Directory mutations are state actions, not commands.** *Resolved (per review):* `workingDirectories` is a keyed collection, so it follows the diff --git a/types/version/registry.ts b/types/version/registry.ts index dd6fb3e87..e2ea83146 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -15,7 +15,7 @@ import type { ServerNotificationMap } from '../messages.js'; * * Formatted as a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH` string. */ -export const PROTOCOL_VERSION = '0.6.1'; +export const PROTOCOL_VERSION = '0.7.0'; /** * Every protocol version a client built from this source tree is willing @@ -34,7 +34,7 @@ export const PROTOCOL_VERSION = '0.6.1'; * `scripts/verify-release-metadata.ts`. */ export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([ - '0.6.1', + '0.7.0', '0.6.0', '0.5.2', '0.5.1', @@ -90,8 +90,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionServerToolsChanged]: '0.1.0', [ActionType.SessionActiveClientSet]: '0.5.0', [ActionType.SessionActiveClientRemoved]: '0.5.0', - [ActionType.SessionWorkingDirectorySet]: '0.6.0', - [ActionType.SessionWorkingDirectoryRemoved]: '0.6.0', + [ActionType.SessionWorkingDirectorySet]: '0.7.0', + [ActionType.SessionWorkingDirectoryRemoved]: '0.7.0', [ActionType.SessionInputNeededSet]: '0.5.1', [ActionType.SessionInputNeededRemoved]: '0.5.1', [ActionType.SessionCustomizationsChanged]: '0.1.0', @@ -123,8 +123,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChatTurnCancelled]: '0.4.0', [ActionType.ChatError]: '0.4.0', [ActionType.ChatActivityChanged]: '0.5.0', - [ActionType.ChatWorkingDirectorySet]: '0.6.0', - [ActionType.ChatWorkingDirectoryRemoved]: '0.6.0', + [ActionType.ChatWorkingDirectorySet]: '0.7.0', + [ActionType.ChatWorkingDirectoryRemoved]: '0.7.0', [ActionType.ChatUsage]: '0.4.0', [ActionType.ChatReasoning]: '0.4.0', [ActionType.ChatPendingMessageSet]: '0.4.0', From 0d715aa60d91a6a0b82f2610acb78e170b2b1d31 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 21 Jul 2026 14:42:39 -0700 Subject: [PATCH 11/15] Revert "spec: target 0.7.0 for multiroot (0.6.0 shipped without it)" This reverts commit 40eb4888771db664227f2d5ec8bb2902328684c1. --- CHANGELOG.md | 4 ++-- clients/go/ahptypes/version.generated.go | 4 ++-- clients/go/release-metadata.json | 2 +- clients/kotlin/release-metadata.json | 2 +- .../generated/Version.generated.kt | 4 ++-- clients/rust/crates/ahp-types/src/version.rs | 4 ++-- clients/rust/release-metadata.json | 2 +- .../Generated/Version.generated.swift | 4 ++-- clients/swift/release-metadata.json | 2 +- clients/typescript/release-metadata.json | 2 +- docs/proposals/multiroot-sessions.md | 23 ++++++++----------- types/version/registry.ts | 12 +++++----- 12 files changed, 31 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d98284c..9cde81419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,9 @@ changes accumulate. Track in-flight protocol changes via PRs touching `NOTIFICATION_INTRODUCED_IN` maps in [`types/version/registry.ts`](types/version/registry.ts). -## [0.7.0] — Unreleased +## [0.6.1] — Unreleased -Spec version: `0.7.0` +Spec version: `0.6.1` ## [0.6.0] — 2026-07-20 diff --git a/clients/go/ahptypes/version.generated.go b/clients/go/ahptypes/version.generated.go index 058869903..26d70813d 100644 --- a/clients/go/ahptypes/version.generated.go +++ b/clients/go/ahptypes/version.generated.go @@ -6,13 +6,13 @@ package ahptypes // ProtocolVersion is the current protocol version (SemVer // MAJOR.MINOR.PATCH) that this generated source speaks. -const ProtocolVersion = "0.7.0" +const ProtocolVersion = "0.6.1" // supportedProtocolVersions backs [SupportedProtocolVersions] — held // in an unexported slice so callers cannot accidentally mutate the // shared backing array. var supportedProtocolVersions = []string{ - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/go/release-metadata.json b/clients/go/release-metadata.json index f26219c29..cb60446f6 100644 --- a/clients/go/release-metadata.json +++ b/clients/go/release-metadata.json @@ -2,7 +2,7 @@ "client": "go", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/kotlin/release-metadata.json b/clients/kotlin/release-metadata.json index c6669d28d..251a45a6b 100644 --- a/clients/kotlin/release-metadata.json +++ b/clients/kotlin/release-metadata.json @@ -2,7 +2,7 @@ "client": "kotlin", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt index bab670ef8..97bcc076b 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt @@ -5,7 +5,7 @@ package com.microsoft.agenthostprotocol.generated /** * Current protocol version (SemVer `MAJOR.MINOR.PATCH`). */ -public const val PROTOCOL_VERSION: String = "0.7.0" +public const val PROTOCOL_VERSION: String = "0.6.1" /** * Every protocol version this library is willing to negotiate, ordered @@ -16,7 +16,7 @@ public const val PROTOCOL_VERSION: String = "0.7.0" * protocol versions if the host doesn't accept the newest one. */ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/rust/crates/ahp-types/src/version.rs b/clients/rust/crates/ahp-types/src/version.rs index d05243be0..edc46573f 100644 --- a/clients/rust/crates/ahp-types/src/version.rs +++ b/clients/rust/crates/ahp-types/src/version.rs @@ -5,7 +5,7 @@ #![allow(missing_docs)] /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -pub const PROTOCOL_VERSION: &str = "0.7.0"; +pub const PROTOCOL_VERSION: &str = "0.6.1"; /// Every protocol version this crate is willing to negotiate, ordered /// most-preferred-first. The first entry equals [`PROTOCOL_VERSION`]. @@ -13,4 +13,4 @@ pub const PROTOCOL_VERSION: &str = "0.7.0"; /// Consumers building `InitializeParams` should pass this slice (or a /// derived `Vec`) so the same client binary can fall back to /// older protocol versions if the host doesn't accept the newest one. -pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.7.0", "0.6.0", "0.5.2", "0.5.1"]; +pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.6.1", "0.6.0", "0.5.2", "0.5.1"]; diff --git a/clients/rust/release-metadata.json b/clients/rust/release-metadata.json index 1fba6f631..3744ab4a4 100644 --- a/clients/rust/release-metadata.json +++ b/clients/rust/release-metadata.json @@ -2,7 +2,7 @@ "client": "rust", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift index e925c5866..902600cd6 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift @@ -3,7 +3,7 @@ import Foundation /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -public let PROTOCOL_VERSION: String = "0.7.0" +public let PROTOCOL_VERSION: String = "0.6.1" /// Every protocol version this package is willing to negotiate, /// ordered most-preferred-first. The first entry equals @@ -13,7 +13,7 @@ public let PROTOCOL_VERSION: String = "0.7.0" /// `InitializeParams` so the same client binary can fall back to older /// protocol versions if the host doesn't accept the newest one. public let SUPPORTED_PROTOCOL_VERSIONS: [String] = [ - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/swift/release-metadata.json b/clients/swift/release-metadata.json index 7aa7042b2..f83f6cee9 100644 --- a/clients/swift/release-metadata.json +++ b/clients/swift/release-metadata.json @@ -2,7 +2,7 @@ "client": "swift", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/typescript/release-metadata.json b/clients/typescript/release-metadata.json index 37c6ac0b8..f53bcb54f 100644 --- a/clients/typescript/release-metadata.json +++ b/clients/typescript/release-metadata.json @@ -2,7 +2,7 @@ "client": "typescript", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.7.0", + "0.6.1", "0.6.0", "0.5.2", "0.5.1" diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index f89f6afd0..90529dcd6 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -219,7 +219,7 @@ flowchart LR The concrete, additive changes that back the framing above — a reviewer's map of the `types/` surface. Full prose detail lives in the [State Model](/guide/state-model#multiroot-sessions) and -[Changesets](/guide/changesets) guides. Target spec version: **0.7.0**. +[Changesets](/guide/changesets) guides. Target spec version: **0.6.0**. Everything here is **additive and optional** — no field is required, no existing field changes type, and old clients that ignore the new surface behave exactly @@ -237,7 +237,7 @@ as today. | `ChatState.workingDirectories?` / `ChatSummary.workingDirectories?` | `channels-chat/state.ts` | added | | `CreateChatParams.workingDirectories?` | `channels-chat/commands.ts` | added | | `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions | `channels-chat/actions.ts` | added (action) | -| 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.7.0`) | `common/actions.ts`, `version/registry.ts` | added | +| 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.6.0`) | `common/actions.ts`, `version/registry.ts` | added | > **Revised after review.** The directory-mutation surface started as four > **commands** (`add/removeWorkspaceFolder`, `add/removeChatWorkspaceFolder` @@ -245,7 +245,7 @@ as today. > following the keyed-collection convention — the set lives in state, so clients > mutate it by dispatching actions and observe the result on > `workingDirectories`. The deprecated singular `workingDirectory` fields were -> **hard-removed** (a breaking change → 0.7.0), not kept as a shorthand. An +> **hard-removed** (0.6.0 is a breaking release), not kept as a shorthand. An > earlier revision also added a `Changeset.workingDirectory` field to group > changes per directory; that was **dropped** — a changeset can span > directories (e.g. a per-turn diff), so a single-directory scalar is the wrong @@ -319,12 +319,11 @@ interface ChatWorkingDirectoryRemovedAction { ### 8.3 Versioning & gating -- `PROTOCOL_VERSION` is **`0.7.0`** — a MINOR bump because the feature is - breaking (it removes the singular `workingDirectory` fields that shipped in the - released `0.6.0`), and pre-1.0 breaking changes land in a MINOR. - `SUPPORTED_PROTOCOL_VERSIONS` = `[0.7.0, 0.6.0, 0.5.2, 0.5.1]`. +- `PROTOCOL_VERSION` is **`0.6.0`** (a breaking release; the branch's base + already advanced the ongoing-dev version to `0.6.0`). `SUPPORTED_PROTOCOL_VERSIONS` + = `[0.6.0, 0.5.2, 0.5.1]`. - The four directory mutations are **state actions**, so they carry - `ACTION_INTRODUCED_IN` entries at `0.7.0` (and are `@clientDispatchable`). + `ACTION_INTRODUCED_IN` entries at `0.6.0` (and are `@clientDispatchable`). Everything else — the capability and the create-time / state fields — is gated by the `multipleWorkingDirectories` capability plus the `initialize` version handshake. @@ -342,12 +341,10 @@ interface ChatWorkingDirectoryRemovedAction { that genuinely pin a fixed process root opt in via `MultipleWorkingDirectoriesCapability.immutablePrimary` (index `0` fixed). -- **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* - removing these fields is a breaking change, so the feature targets a MINOR - bump (`0.7.0`); the deprecated singular fields on +- **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* 0.6.0 + is a breaking release, so the deprecated singular fields on `CreateSessionParams` / `SessionMetadata` / `ChatState` / `ChatSummary` are - removed outright rather than kept as a shorthand. (Originally planned to ride - `0.6.0`'s breaking window, but `0.6.0` shipped without this feature.) + removed outright rather than kept as a shorthand. - **Directory mutations are state actions, not commands.** *Resolved (per review):* `workingDirectories` is a keyed collection, so it follows the diff --git a/types/version/registry.ts b/types/version/registry.ts index e2ea83146..dd6fb3e87 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -15,7 +15,7 @@ import type { ServerNotificationMap } from '../messages.js'; * * Formatted as a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH` string. */ -export const PROTOCOL_VERSION = '0.7.0'; +export const PROTOCOL_VERSION = '0.6.1'; /** * Every protocol version a client built from this source tree is willing @@ -34,7 +34,7 @@ export const PROTOCOL_VERSION = '0.7.0'; * `scripts/verify-release-metadata.ts`. */ export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([ - '0.7.0', + '0.6.1', '0.6.0', '0.5.2', '0.5.1', @@ -90,8 +90,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionServerToolsChanged]: '0.1.0', [ActionType.SessionActiveClientSet]: '0.5.0', [ActionType.SessionActiveClientRemoved]: '0.5.0', - [ActionType.SessionWorkingDirectorySet]: '0.7.0', - [ActionType.SessionWorkingDirectoryRemoved]: '0.7.0', + [ActionType.SessionWorkingDirectorySet]: '0.6.0', + [ActionType.SessionWorkingDirectoryRemoved]: '0.6.0', [ActionType.SessionInputNeededSet]: '0.5.1', [ActionType.SessionInputNeededRemoved]: '0.5.1', [ActionType.SessionCustomizationsChanged]: '0.1.0', @@ -123,8 +123,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChatTurnCancelled]: '0.4.0', [ActionType.ChatError]: '0.4.0', [ActionType.ChatActivityChanged]: '0.5.0', - [ActionType.ChatWorkingDirectorySet]: '0.7.0', - [ActionType.ChatWorkingDirectoryRemoved]: '0.7.0', + [ActionType.ChatWorkingDirectorySet]: '0.6.0', + [ActionType.ChatWorkingDirectoryRemoved]: '0.6.0', [ActionType.ChatUsage]: '0.4.0', [ActionType.ChatReasoning]: '0.4.0', [ActionType.ChatPendingMessageSet]: '0.4.0', From d7c7d5a3d8d3a9a10198dd315f5e85d2a4e43c8d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 21 Jul 2026 14:47:41 -0700 Subject: [PATCH 12/15] Reapply "spec: target 0.7.0 for multiroot (0.6.0 shipped without it)" This reverts commit 0d715aa60d91a6a0b82f2610acb78e170b2b1d31. --- CHANGELOG.md | 4 ++-- clients/go/ahptypes/version.generated.go | 4 ++-- clients/go/release-metadata.json | 2 +- clients/kotlin/release-metadata.json | 2 +- .../generated/Version.generated.kt | 4 ++-- clients/rust/crates/ahp-types/src/version.rs | 4 ++-- clients/rust/release-metadata.json | 2 +- .../Generated/Version.generated.swift | 4 ++-- clients/swift/release-metadata.json | 2 +- clients/typescript/release-metadata.json | 2 +- docs/proposals/multiroot-sessions.md | 23 +++++++++++-------- types/version/registry.ts | 12 +++++----- 12 files changed, 34 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cde81419..f2d98284c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,9 @@ changes accumulate. Track in-flight protocol changes via PRs touching `NOTIFICATION_INTRODUCED_IN` maps in [`types/version/registry.ts`](types/version/registry.ts). -## [0.6.1] — Unreleased +## [0.7.0] — Unreleased -Spec version: `0.6.1` +Spec version: `0.7.0` ## [0.6.0] — 2026-07-20 diff --git a/clients/go/ahptypes/version.generated.go b/clients/go/ahptypes/version.generated.go index 26d70813d..058869903 100644 --- a/clients/go/ahptypes/version.generated.go +++ b/clients/go/ahptypes/version.generated.go @@ -6,13 +6,13 @@ package ahptypes // ProtocolVersion is the current protocol version (SemVer // MAJOR.MINOR.PATCH) that this generated source speaks. -const ProtocolVersion = "0.6.1" +const ProtocolVersion = "0.7.0" // supportedProtocolVersions backs [SupportedProtocolVersions] — held // in an unexported slice so callers cannot accidentally mutate the // shared backing array. var supportedProtocolVersions = []string{ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/go/release-metadata.json b/clients/go/release-metadata.json index cb60446f6..f26219c29 100644 --- a/clients/go/release-metadata.json +++ b/clients/go/release-metadata.json @@ -2,7 +2,7 @@ "client": "go", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/kotlin/release-metadata.json b/clients/kotlin/release-metadata.json index 251a45a6b..c6669d28d 100644 --- a/clients/kotlin/release-metadata.json +++ b/clients/kotlin/release-metadata.json @@ -2,7 +2,7 @@ "client": "kotlin", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt index 97bcc076b..bab670ef8 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Version.generated.kt @@ -5,7 +5,7 @@ package com.microsoft.agenthostprotocol.generated /** * Current protocol version (SemVer `MAJOR.MINOR.PATCH`). */ -public const val PROTOCOL_VERSION: String = "0.6.1" +public const val PROTOCOL_VERSION: String = "0.7.0" /** * Every protocol version this library is willing to negotiate, ordered @@ -16,7 +16,7 @@ public const val PROTOCOL_VERSION: String = "0.6.1" * protocol versions if the host doesn't accept the newest one. */ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/rust/crates/ahp-types/src/version.rs b/clients/rust/crates/ahp-types/src/version.rs index edc46573f..d05243be0 100644 --- a/clients/rust/crates/ahp-types/src/version.rs +++ b/clients/rust/crates/ahp-types/src/version.rs @@ -5,7 +5,7 @@ #![allow(missing_docs)] /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -pub const PROTOCOL_VERSION: &str = "0.6.1"; +pub const PROTOCOL_VERSION: &str = "0.7.0"; /// Every protocol version this crate is willing to negotiate, ordered /// most-preferred-first. The first entry equals [`PROTOCOL_VERSION`]. @@ -13,4 +13,4 @@ pub const PROTOCOL_VERSION: &str = "0.6.1"; /// Consumers building `InitializeParams` should pass this slice (or a /// derived `Vec`) so the same client binary can fall back to /// older protocol versions if the host doesn't accept the newest one. -pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.6.1", "0.6.0", "0.5.2", "0.5.1"]; +pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["0.7.0", "0.6.0", "0.5.2", "0.5.1"]; diff --git a/clients/rust/release-metadata.json b/clients/rust/release-metadata.json index 3744ab4a4..1fba6f631 100644 --- a/clients/rust/release-metadata.json +++ b/clients/rust/release-metadata.json @@ -2,7 +2,7 @@ "client": "rust", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift index 902600cd6..e925c5866 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Version.generated.swift @@ -3,7 +3,7 @@ import Foundation /// Current protocol version (SemVer `MAJOR.MINOR.PATCH`). -public let PROTOCOL_VERSION: String = "0.6.1" +public let PROTOCOL_VERSION: String = "0.7.0" /// Every protocol version this package is willing to negotiate, /// ordered most-preferred-first. The first entry equals @@ -13,7 +13,7 @@ public let PROTOCOL_VERSION: String = "0.6.1" /// `InitializeParams` so the same client binary can fall back to older /// protocol versions if the host doesn't accept the newest one. public let SUPPORTED_PROTOCOL_VERSIONS: [String] = [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1", diff --git a/clients/swift/release-metadata.json b/clients/swift/release-metadata.json index f83f6cee9..7aa7042b2 100644 --- a/clients/swift/release-metadata.json +++ b/clients/swift/release-metadata.json @@ -2,7 +2,7 @@ "client": "swift", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/clients/typescript/release-metadata.json b/clients/typescript/release-metadata.json index f53bcb54f..37c6ac0b8 100644 --- a/clients/typescript/release-metadata.json +++ b/clients/typescript/release-metadata.json @@ -2,7 +2,7 @@ "client": "typescript", "packageVersion": "0.6.0", "supportedProtocolVersions": [ - "0.6.1", + "0.7.0", "0.6.0", "0.5.2", "0.5.1" diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index 90529dcd6..f89f6afd0 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -219,7 +219,7 @@ flowchart LR The concrete, additive changes that back the framing above — a reviewer's map of the `types/` surface. Full prose detail lives in the [State Model](/guide/state-model#multiroot-sessions) and -[Changesets](/guide/changesets) guides. Target spec version: **0.6.0**. +[Changesets](/guide/changesets) guides. Target spec version: **0.7.0**. Everything here is **additive and optional** — no field is required, no existing field changes type, and old clients that ignore the new surface behave exactly @@ -237,7 +237,7 @@ as today. | `ChatState.workingDirectories?` / `ChatSummary.workingDirectories?` | `channels-chat/state.ts` | added | | `CreateChatParams.workingDirectories?` | `channels-chat/commands.ts` | added | | `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions | `channels-chat/actions.ts` | added (action) | -| 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.6.0`) | `common/actions.ts`, `version/registry.ts` | added | +| 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.7.0`) | `common/actions.ts`, `version/registry.ts` | added | > **Revised after review.** The directory-mutation surface started as four > **commands** (`add/removeWorkspaceFolder`, `add/removeChatWorkspaceFolder` @@ -245,7 +245,7 @@ as today. > following the keyed-collection convention — the set lives in state, so clients > mutate it by dispatching actions and observe the result on > `workingDirectories`. The deprecated singular `workingDirectory` fields were -> **hard-removed** (0.6.0 is a breaking release), not kept as a shorthand. An +> **hard-removed** (a breaking change → 0.7.0), not kept as a shorthand. An > earlier revision also added a `Changeset.workingDirectory` field to group > changes per directory; that was **dropped** — a changeset can span > directories (e.g. a per-turn diff), so a single-directory scalar is the wrong @@ -319,11 +319,12 @@ interface ChatWorkingDirectoryRemovedAction { ### 8.3 Versioning & gating -- `PROTOCOL_VERSION` is **`0.6.0`** (a breaking release; the branch's base - already advanced the ongoing-dev version to `0.6.0`). `SUPPORTED_PROTOCOL_VERSIONS` - = `[0.6.0, 0.5.2, 0.5.1]`. +- `PROTOCOL_VERSION` is **`0.7.0`** — a MINOR bump because the feature is + breaking (it removes the singular `workingDirectory` fields that shipped in the + released `0.6.0`), and pre-1.0 breaking changes land in a MINOR. + `SUPPORTED_PROTOCOL_VERSIONS` = `[0.7.0, 0.6.0, 0.5.2, 0.5.1]`. - The four directory mutations are **state actions**, so they carry - `ACTION_INTRODUCED_IN` entries at `0.6.0` (and are `@clientDispatchable`). + `ACTION_INTRODUCED_IN` entries at `0.7.0` (and are `@clientDispatchable`). Everything else — the capability and the create-time / state fields — is gated by the `multipleWorkingDirectories` capability plus the `initialize` version handshake. @@ -341,10 +342,12 @@ interface ChatWorkingDirectoryRemovedAction { that genuinely pin a fixed process root opt in via `MultipleWorkingDirectoriesCapability.immutablePrimary` (index `0` fixed). -- **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* 0.6.0 - is a breaking release, so the deprecated singular fields on +- **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* + removing these fields is a breaking change, so the feature targets a MINOR + bump (`0.7.0`); the deprecated singular fields on `CreateSessionParams` / `SessionMetadata` / `ChatState` / `ChatSummary` are - removed outright rather than kept as a shorthand. + removed outright rather than kept as a shorthand. (Originally planned to ride + `0.6.0`'s breaking window, but `0.6.0` shipped without this feature.) - **Directory mutations are state actions, not commands.** *Resolved (per review):* `workingDirectories` is a keyed collection, so it follows the diff --git a/types/version/registry.ts b/types/version/registry.ts index dd6fb3e87..e2ea83146 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -15,7 +15,7 @@ import type { ServerNotificationMap } from '../messages.js'; * * Formatted as a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH` string. */ -export const PROTOCOL_VERSION = '0.6.1'; +export const PROTOCOL_VERSION = '0.7.0'; /** * Every protocol version a client built from this source tree is willing @@ -34,7 +34,7 @@ export const PROTOCOL_VERSION = '0.6.1'; * `scripts/verify-release-metadata.ts`. */ export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([ - '0.6.1', + '0.7.0', '0.6.0', '0.5.2', '0.5.1', @@ -90,8 +90,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionServerToolsChanged]: '0.1.0', [ActionType.SessionActiveClientSet]: '0.5.0', [ActionType.SessionActiveClientRemoved]: '0.5.0', - [ActionType.SessionWorkingDirectorySet]: '0.6.0', - [ActionType.SessionWorkingDirectoryRemoved]: '0.6.0', + [ActionType.SessionWorkingDirectorySet]: '0.7.0', + [ActionType.SessionWorkingDirectoryRemoved]: '0.7.0', [ActionType.SessionInputNeededSet]: '0.5.1', [ActionType.SessionInputNeededRemoved]: '0.5.1', [ActionType.SessionCustomizationsChanged]: '0.1.0', @@ -123,8 +123,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChatTurnCancelled]: '0.4.0', [ActionType.ChatError]: '0.4.0', [ActionType.ChatActivityChanged]: '0.5.0', - [ActionType.ChatWorkingDirectorySet]: '0.6.0', - [ActionType.ChatWorkingDirectoryRemoved]: '0.6.0', + [ActionType.ChatWorkingDirectorySet]: '0.7.0', + [ActionType.ChatWorkingDirectoryRemoved]: '0.7.0', [ActionType.ChatUsage]: '0.4.0', [ActionType.ChatReasoning]: '0.4.0', [ActionType.ChatPendingMessageSet]: '0.4.0', From 148c716b8a23ac8f3ca71635e4b72ceed2b6d571 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 21 Jul 2026 17:13:33 -0700 Subject: [PATCH 13/15] spec: rename requiresPrimary + add primaryWorkingDirectory (session + chat) Per finalized design: - Rename the capability flag immutablePrimary -> requiresPrimary. It now means "the agent needs one directory designated as primary" (a distinguished root), rather than "index 0 is a fixed process root". Primacy is explicit, never inferred from array position. - Add primaryWorkingDirectory (URI) to CreateSessionParams + SessionMetadata (-> SessionState/SessionSummary) and to CreateChatParams + ChatState/ ChatSummary. A chat inherits the session's primary when it omits its own. - Update hand-written client reducers/mergers (Go/Rust/Kotlin/Swift) to carry primaryWorkingDirectory through session/chatUpdated, add the field to Go's hand-maintained PartialChatSummary, and fix Rust test struct literals. - Docs (state-model, proposal) and changelog fragment updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahp/reducers.go | 3 + clients/go/ahptypes/actions.generated.go | 6 +- clients/go/ahptypes/commands.generated.go | 21 ++++- clients/go/ahptypes/common.go | 2 + .../go/ahptypes/notifications.generated.go | 16 ++-- clients/go/ahptypes/state.generated.go | 69 ++++++++++----- .../microsoft/agenthostprotocol/Reducers.kt | 1 + .../generated/Actions.generated.kt | 7 +- .../generated/Commands.generated.kt | 27 +++++- .../generated/Notifications.generated.kt | 18 ++-- .../generated/State.generated.kt | 79 ++++++++++++----- clients/rust/crates/ahp-types/src/actions.rs | 10 ++- clients/rust/crates/ahp-types/src/commands.rs | 23 ++++- .../crates/ahp-types/src/notifications.rs | 17 ++-- clients/rust/crates/ahp-types/src/state.rs | 73 +++++++++++----- clients/rust/crates/ahp/src/reducers.rs | 6 ++ clients/rust/crates/ahp/tests/hosts.rs | 1 + .../ahp/tests/multi_host_state_mirror.rs | 2 + .../Generated/Actions.generated.swift | 7 +- .../Generated/Commands.generated.swift | 27 +++++- .../Generated/Notifications.generated.swift | 19 ++-- .../Generated/State.generated.swift | 86 +++++++++++++------ .../Sources/AgentHostProtocol/Reducers.swift | 1 + .../.changes/20260716-multiroot-sessions.json | 2 +- docs/guide/state-model.md | 28 ++++-- docs/proposals/multiroot-sessions.md | 47 ++++++---- schema/actions.schema.json | 38 ++++++-- schema/commands.schema.json | 48 +++++++++-- schema/errors.schema.json | 48 +++++++++-- schema/notifications.schema.json | 38 ++++++-- schema/state.schema.json | 32 +++++-- types/channels-chat/commands.ts | 10 +++ types/channels-chat/state.ts | 15 ++++ types/channels-root/state.ts | 26 +++--- types/channels-session/actions.ts | 6 +- types/channels-session/commands.ts | 15 +++- types/channels-session/state.ts | 18 ++-- 37 files changed, 678 insertions(+), 214 deletions(-) diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 43f659a4b..364801d58 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -770,6 +770,9 @@ func mergeChatSummaryPartial(summary *ahptypes.ChatSummary, changes ahptypes.Par if changes.WorkingDirectories != nil { summary.WorkingDirectories = changes.WorkingDirectories } + if changes.PrimaryWorkingDirectory != nil { + summary.PrimaryWorkingDirectory = changes.PrimaryWorkingDirectory + } } // ─── Session Reducer ─────────────────────────────────────────────────── diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 137503b7f..b4ce2edb8 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -898,9 +898,9 @@ type SessionWorkingDirectorySetAction struct { // Removes `directory` from the set; a no-op when it is not present. There is no // atomic backend "remove one" primitive — a host reconfigures its agent to the // reduced set — so this action is safe to model as idempotent. A host MAY -// decline to apply the removal (e.g. an immutable primary directory, see -// {@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves -// the set unchanged. +// decline to apply the removal (e.g. the current primary directory of an agent +// that {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a +// primary}); it then leaves the set unchanged. type SessionWorkingDirectoryRemovedAction struct { Type ActionType `json:"type"` // The working directory to revoke the session's agent tool access to. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 33a431e1c..3f79a05da 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -308,8 +308,9 @@ type CreateSessionParams struct { // The working directories the session's agent is granted tool access to. // A session may span multiple directories; they are equal peers except when // the agent advertises - // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case - // the first entry is a fixed process root). + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + // one of them should be designated the primary via + // {@link primaryWorkingDirectory}. // // A client MUST NOT supply more than one entry unless the agent advertises // {@link AgentCapabilities.multipleWorkingDirectories}; a server without that @@ -321,6 +322,14 @@ type CreateSessionParams struct { // Ignored for forked sessions — a fork inherits its working directories // from the source session identified by `fork`. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The session's primary working directory — the distinguished root the agent + // centers on. When set, it MUST be one of {@link workingDirectories}. A client + // SHOULD supply this when the agent advertises + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + // reject creation that omits it, or fall back to the first entry of + // `workingDirectories`. Ignored for forked sessions (a fork inherits the + // source session's primary). + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Fork from an existing session. The new session is populated with content // from the source session up to and including the specified turn's response. Fork *SessionForkSource `json:"fork,omitempty"` @@ -382,6 +391,14 @@ type CreateChatParams struct { // A client MUST NOT supply this field unless the agent advertises // {@link AgentCapabilities.multipleWorkingDirectories}. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The chat's primary working directory — the distinguished root this chat + // centers on. When set, it MUST be one of the chat's effective working + // directories ({@link workingDirectories}, or the session's set when that is + // omitted). A client SHOULD supply this when the agent advertises + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat + // narrows to a subset that excludes the session's primary; when absent, the + // chat inherits the session's primary. Ignored for forked chats. + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` } // Disposes a chat and cleans up server-side resources. diff --git a/clients/go/ahptypes/common.go b/clients/go/ahptypes/common.go index 96792a2ac..233051bbf 100644 --- a/clients/go/ahptypes/common.go +++ b/clients/go/ahptypes/common.go @@ -85,6 +85,8 @@ type PartialChatSummary struct { Origin *ChatOrigin `json:"origin,omitempty"` // The subset of the session's working directories this chat uses. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The chat's primary working directory. + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` } // ─── StringOrMarkdown ──────────────────────────────────────────────────── diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 34e0908dd..31f7508b2 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -221,12 +221,18 @@ type PartialSessionSummary struct { // maintained by the `session/workingDirectorySet` / // `session/workingDirectoryRemoved` actions. Directories are equal peers // except when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - // entry is then a fixed process root). Individual chats MAY restrict to a - // subset via {@link ChatSummary.workingDirectories | their own - // `workingDirectories`}; a chat that sets none operates against this full - // set. + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + // one of them is designated the primary (see {@link primaryWorkingDirectory}). + // Individual chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The session's primary working directory — the distinguished root the agent + // centers on. When set, it MUST be one of {@link workingDirectories}. Present + // when the agent advertises + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + // the agent has no primary (all directories equal peers). + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index da52367b4..f7c3b755a 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -586,8 +586,8 @@ type AgentCapabilities struct { MultipleChats *MultipleChatsCapability `json:"multipleChats,omitempty"` // The session's agent can be granted tool access to more than one working // directory. The directories are treated as equal peers except where the - // agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} - // (some backends pin their first directory as a fixed process root). + // agent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary} + // (some backends need one directory designated as a primary root). // // When absent, clients MUST NOT mutate a session's or chat's working-directory // set and MUST NOT set more than one entry in @@ -605,17 +605,19 @@ type MultipleChatsCapability struct { // Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. type MultipleWorkingDirectoriesCapability struct { - // The agent's **first** working directory (index `0` of - // {@link CreateSessionParams.workingDirectories}) is an immutable primary: - // it is fixed for the lifetime of the session — clients MUST NOT remove or - // reorder it. Additional directories after it remain equal peers that can be - // added and removed freely. + // The agent requires exactly one of its working directories to be designated + // the **primary** — a distinguished root the agent centers on (e.g. its + // process root, the default location for relative paths). When `true`, a + // client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} + // (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that + // narrows the set); a host MAY reject creation that omits it, or fall back to + // the first entry of `workingDirectories`. The chosen primary is reported on + // {@link SessionState.primaryWorkingDirectory} / + // {@link ChatState.primaryWorkingDirectory}. // - // Advertised by backends whose agent process is rooted at a single directory - // that cannot change once the session has started (e.g. the SDK's primary - // `workingDirectory`). When absent or `false`, all directories are equal - // peers and any of them may be removed. - ImmutablePrimary *bool `json:"immutablePrimary,omitempty"` + // When absent or `false`, the agent has no primary — all directories are + // equal peers and clients need not designate one. + RequiresPrimary *bool `json:"requiresPrimary,omitempty"` } type SessionModelInfo struct { @@ -742,12 +744,18 @@ type SessionState struct { // maintained by the `session/workingDirectorySet` / // `session/workingDirectoryRemoved` actions. Directories are equal peers // except when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - // entry is then a fixed process root). Individual chats MAY restrict to a - // subset via {@link ChatSummary.workingDirectories | their own - // `workingDirectories`}; a chat that sets none operates against this full - // set. + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + // one of them is designated the primary (see {@link primaryWorkingDirectory}). + // Individual chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The session's primary working directory — the distinguished root the agent + // centers on. When set, it MUST be one of {@link workingDirectories}. Present + // when the agent advertises + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + // the agent has no primary (all directories equal peers). + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -1008,12 +1016,18 @@ type SessionSummary struct { // maintained by the `session/workingDirectorySet` / // `session/workingDirectoryRemoved` actions. Directories are equal peers // except when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - // entry is then a fixed process root). Individual chats MAY restrict to a - // subset via {@link ChatSummary.workingDirectories | their own - // `workingDirectories`}; a chat that sets none operates against this full - // set. + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + // one of them is designated the primary (see {@link primaryWorkingDirectory}). + // Individual chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The session's primary working directory — the distinguished root the agent + // centers on. When set, it MUST be one of {@link workingDirectories}. Present + // when the agent advertises + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + // the agent has no primary (all directories equal peers). + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -1092,6 +1106,14 @@ type ChatState struct { // Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to // update the subset on a running chat. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The chat's primary working directory — the distinguished root this chat + // centers on. When set, it MUST be one of this chat's effective working + // directories ({@link workingDirectories}, or the session's set when that is + // absent). Present when the agent advertises + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, + // the chat inherits {@link SessionState.primaryWorkingDirectory | the + // session's primary}. + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Completed turns Turns []Turn `json:"turns"` // Cursor for loading older completed turns into this chat state. @@ -1148,6 +1170,9 @@ type ChatSummary struct { // The subset of the session's working directories this chat uses. // See {@link ChatState.workingDirectories} for the full semantics. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // The chat's primary working directory. + // See {@link ChatState.primaryWorkingDirectory} for the full semantics. + PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` } // A message queued for future delivery to the agent. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index e681f91fb..95a751e77 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -546,6 +546,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat modifiedAt = c.modifiedAt ?: prior.modifiedAt, origin = c.origin ?: prior.origin, workingDirectories = c.workingDirectories ?: prior.workingDirectories, + primaryWorkingDirectory = c.primaryWorkingDirectory ?: prior.primaryWorkingDirectory, ) val updated = state.chats.toMutableList() updated[idx] = updatedSummary diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index 13c2ce9d6..1dd8e4709 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -1510,7 +1510,12 @@ data class PartialChatSummary( * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. */ - val workingDirectories: List? = null + val workingDirectories: List? = null, + /** + * The chat's primary working directory. + * See {@link ChatState.primaryWorkingDirectory} for the full semantics. + */ + val primaryWorkingDirectory: String? = null ) // ─── StateAction Union ────────────────────────────────────────────────────── diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 3238ea9eb..5f0431cfd 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -372,8 +372,9 @@ data class CreateSessionParams( * The working directories the session's agent is granted tool access to. * A session may span multiple directories; they are equal peers except when * the agent advertises - * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case - * the first entry is a fixed process root). + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + * one of them should be designated the primary via + * {@link primaryWorkingDirectory}. * * A client MUST NOT supply more than one entry unless the agent advertises * {@link AgentCapabilities.multipleWorkingDirectories}; a server without that @@ -386,6 +387,16 @@ data class CreateSessionParams( * from the source session identified by `fork`. */ val workingDirectories: List? = null, + /** + * The session's primary working directory — the distinguished root the agent + * centers on. When set, it MUST be one of {@link workingDirectories}. A client + * SHOULD supply this when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + * reject creation that omits it, or fall back to the first entry of + * `workingDirectories`. Ignored for forked sessions (a fork inherits the + * source session's primary). + */ + val primaryWorkingDirectory: String? = null, /** * Fork from an existing session. The new session is populated with content * from the source session up to and including the specified turn's response. @@ -468,7 +479,17 @@ data class CreateChatParams( * A client MUST NOT supply this field unless the agent advertises * {@link AgentCapabilities.multipleWorkingDirectories}. */ - val workingDirectories: List? = null + val workingDirectories: List? = null, + /** + * The chat's primary working directory — the distinguished root this chat + * centers on. When set, it MUST be one of the chat's effective working + * directories ({@link workingDirectories}, or the session's set when that is + * omitted). A client SHOULD supply this when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat + * narrows to a subset that excludes the session's primary; when absent, the + * chat inherits the session's primary. Ignored for forked chats. + */ + val primaryWorkingDirectory: String? = null ) @Serializable diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 64e93a151..09b2d3759 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -200,13 +200,21 @@ data class PartialSessionSummary( * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - * entry is then a fixed process root). Individual chats MAY restrict to a - * subset via {@link ChatSummary.workingDirectories | their own - * `workingDirectories`}; a chat that sets none operates against this full - * set. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + * one of them is designated the primary (see {@link primaryWorkingDirectory}). + * Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + * chat that sets none operates against this full set. */ val workingDirectories: List? = null, + /** + * The session's primary working directory — the distinguished root the agent + * centers on. When set, it MUST be one of {@link workingDirectories}. Present + * when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + * the agent has no primary (all directories equal peers). + */ + val primaryWorkingDirectory: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index ad0b4cce6..2b39c6514 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -956,8 +956,8 @@ data class AgentCapabilities( /** * The session's agent can be granted tool access to more than one working * directory. The directories are treated as equal peers except where the - * agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} - * (some backends pin their first directory as a fixed process root). + * agent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary} + * (some backends need one directory designated as a primary root). * * When absent, clients MUST NOT mutate a session's or chat's working-directory * set and MUST NOT set more than one entry in @@ -979,18 +979,20 @@ data class MultipleChatsCapability( @Serializable data class MultipleWorkingDirectoriesCapability( /** - * The agent's **first** working directory (index `0` of - * {@link CreateSessionParams.workingDirectories}) is an immutable primary: - * it is fixed for the lifetime of the session — clients MUST NOT remove or - * reorder it. Additional directories after it remain equal peers that can be - * added and removed freely. + * The agent requires exactly one of its working directories to be designated + * the **primary** — a distinguished root the agent centers on (e.g. its + * process root, the default location for relative paths). When `true`, a + * client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} + * (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that + * narrows the set); a host MAY reject creation that omits it, or fall back to + * the first entry of `workingDirectories`. The chosen primary is reported on + * {@link SessionState.primaryWorkingDirectory} / + * {@link ChatState.primaryWorkingDirectory}. * - * Advertised by backends whose agent process is rooted at a single directory - * that cannot change once the session has started (e.g. the SDK's primary - * `workingDirectory`). When absent or `false`, all directories are equal - * peers and any of them may be removed. + * When absent or `false`, the agent has no primary — all directories are + * equal peers and clients need not designate one. */ - val immutablePrimary: Boolean? = null + val requiresPrimary: Boolean? = null ) @Serializable @@ -1193,6 +1195,16 @@ data class ChatState( * update the subset on a running chat. */ val workingDirectories: List? = null, + /** + * The chat's primary working directory — the distinguished root this chat + * centers on. When set, it MUST be one of this chat's effective working + * directories ({@link workingDirectories}, or the session's set when that is + * absent). Present when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, + * the chat inherits {@link SessionState.primaryWorkingDirectory | the + * session's primary}. + */ + val primaryWorkingDirectory: String? = null, /** * Completed turns */ @@ -1277,7 +1289,12 @@ data class ChatSummary( * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. */ - val workingDirectories: List? = null + val workingDirectories: List? = null, + /** + * The chat's primary working directory. + * See {@link ChatState.primaryWorkingDirectory} for the full semantics. + */ + val primaryWorkingDirectory: String? = null ) @Serializable @@ -1307,13 +1324,21 @@ data class SessionState( * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - * entry is then a fixed process root). Individual chats MAY restrict to a - * subset via {@link ChatSummary.workingDirectories | their own - * `workingDirectories`}; a chat that sets none operates against this full - * set. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + * one of them is designated the primary (see {@link primaryWorkingDirectory}). + * Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + * chat that sets none operates against this full set. */ val workingDirectories: List? = null, + /** + * The session's primary working directory — the distinguished root the agent + * centers on. When set, it MUST be one of {@link workingDirectories}. Present + * when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + * the agent has no primary (all directories equal peers). + */ + val primaryWorkingDirectory: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1581,13 +1606,21 @@ data class SessionSummary( * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - * entry is then a fixed process root). Individual chats MAY restrict to a - * subset via {@link ChatSummary.workingDirectories | their own - * `workingDirectories`}; a chat that sets none operates against this full - * set. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + * one of them is designated the primary (see {@link primaryWorkingDirectory}). + * Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + * chat that sets none operates against this full set. */ val workingDirectories: List? = null, + /** + * The session's primary working directory — the distinguished root the agent + * centers on. When set, it MUST be one of {@link workingDirectories}. Present + * when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + * the agent has no primary (all directories equal peers). + */ + val primaryWorkingDirectory: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 9d733fcae..776ecf1a5 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -954,9 +954,9 @@ pub struct SessionWorkingDirectorySetAction { /// Removes `directory` from the set; a no-op when it is not present. There is no /// atomic backend "remove one" primitive — a host reconfigures its agent to the /// reduced set — so this action is safe to model as idempotent. A host MAY -/// decline to apply the removal (e.g. an immutable primary directory, see -/// {@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves -/// the set unchanged. +/// decline to apply the removal (e.g. the current primary directory of an agent +/// that {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a +/// primary}); it then leaves the set unchanged. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionWorkingDirectoryRemovedAction { @@ -1762,6 +1762,10 @@ pub struct PartialChatSummary { /// See {@link ChatState.workingDirectories} for the full semantics. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The chat's primary working directory. + /// See {@link ChatState.primaryWorkingDirectory} for the full semantics. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, } // ─── StateAction Union ─────────────────────────────────────────────── diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index c08c47a73..f49b30fcd 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -394,8 +394,9 @@ pub struct CreateSessionParams { /// The working directories the session's agent is granted tool access to. /// A session may span multiple directories; they are equal peers except when /// the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case - /// the first entry is a fixed process root). + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them should be designated the primary via + /// {@link primaryWorkingDirectory}. /// /// A client MUST NOT supply more than one entry unless the agent advertises /// {@link AgentCapabilities.multipleWorkingDirectories}; a server without that @@ -408,6 +409,15 @@ pub struct CreateSessionParams { /// from the source session identified by `fork`. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. A client + /// SHOULD supply this when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + /// reject creation that omits it, or fall back to the first entry of + /// `workingDirectories`. Ignored for forked sessions (a fork inherits the + /// source session's primary). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, /// Fork from an existing session. The new session is populated with content /// from the source session up to and including the specified turn's response. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -482,6 +492,15 @@ pub struct CreateChatParams { /// {@link AgentCapabilities.multipleWorkingDirectories}. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The chat's primary working directory — the distinguished root this chat + /// centers on. When set, it MUST be one of the chat's effective working + /// directories ({@link workingDirectories}, or the session's set when that is + /// omitted). A client SHOULD supply this when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat + /// narrows to a subset that excludes the session's primary; when absent, the + /// chat inherits the session's primary. Ignored for forked chats. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, } /// Disposes a chat and cleans up server-side resources. diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index e54e93769..afa106164 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -252,13 +252,20 @@ pub struct PartialSessionSummary { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - /// entry is then a fixed process root). Individual chats MAY restrict to a - /// subset via {@link ChatSummary.workingDirectories | their own - /// `workingDirectories`}; a chat that sets none operates against this full - /// set. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them is designated the primary (see {@link primaryWorkingDirectory}). + /// Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. Present + /// when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + /// the agent has no primary (all directories equal peers). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 02735acc9..0c6b5ae42 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -805,8 +805,8 @@ pub struct AgentCapabilities { pub multiple_chats: Option, /// The session's agent can be granted tool access to more than one working /// directory. The directories are treated as equal peers except where the - /// agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} - /// (some backends pin their first directory as a fixed process root). + /// agent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary} + /// (some backends need one directory designated as a primary root). /// /// When absent, clients MUST NOT mutate a session's or chat's working-directory /// set and MUST NOT set more than one entry in @@ -830,18 +830,20 @@ pub struct MultipleChatsCapability { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct MultipleWorkingDirectoriesCapability { - /// The agent's **first** working directory (index `0` of - /// {@link CreateSessionParams.workingDirectories}) is an immutable primary: - /// it is fixed for the lifetime of the session — clients MUST NOT remove or - /// reorder it. Additional directories after it remain equal peers that can be - /// added and removed freely. + /// The agent requires exactly one of its working directories to be designated + /// the **primary** — a distinguished root the agent centers on (e.g. its + /// process root, the default location for relative paths). When `true`, a + /// client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} + /// (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that + /// narrows the set); a host MAY reject creation that omits it, or fall back to + /// the first entry of `workingDirectories`. The chosen primary is reported on + /// {@link SessionState.primaryWorkingDirectory} / + /// {@link ChatState.primaryWorkingDirectory}. /// - /// Advertised by backends whose agent process is rooted at a single directory - /// that cannot change once the session has started (e.g. the SDK's primary - /// `workingDirectory`). When absent or `false`, all directories are equal - /// peers and any of them may be removed. + /// When absent or `false`, the agent has no primary — all directories are + /// equal peers and clients need not designate one. #[serde(default, skip_serializing_if = "Option::is_none")] - pub immutable_primary: Option, + pub requires_primary: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -1037,6 +1039,15 @@ pub struct ChatState { /// update the subset on a running chat. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The chat's primary working directory — the distinguished root this chat + /// centers on. When set, it MUST be one of this chat's effective working + /// directories ({@link workingDirectories}, or the session's set when that is + /// absent). Present when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, + /// the chat inherits {@link SessionState.primaryWorkingDirectory | the + /// session's primary}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, /// Completed turns pub turns: Vec, /// Cursor for loading older completed turns into this chat state. @@ -1105,6 +1116,10 @@ pub struct ChatSummary { /// See {@link ChatState.workingDirectories} for the full semantics. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The chat's primary working directory. + /// See {@link ChatState.primaryWorkingDirectory} for the full semantics. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, } /// Full state for a single session, loaded when a client subscribes to the session's URI. @@ -1133,13 +1148,20 @@ pub struct SessionState { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - /// entry is then a fixed process root). Individual chats MAY restrict to a - /// subset via {@link ChatSummary.workingDirectories | their own - /// `workingDirectories`}; a chat that sets none operates against this full - /// set. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them is designated the primary (see {@link primaryWorkingDirectory}). + /// Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. Present + /// when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + /// the agent has no primary (all directories equal peers). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1421,13 +1443,20 @@ pub struct SessionSummary { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - /// entry is then a fixed process root). Individual chats MAY restrict to a - /// subset via {@link ChatSummary.workingDirectories | their own - /// `workingDirectories`}; a chat that sets none operates against this full - /// set. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them is designated the primary (see {@link primaryWorkingDirectory}). + /// Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. Present + /// when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + /// the agent has no primary (all directories equal peers). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_working_directory: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index b5cbd1884..5392beead 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -670,6 +670,9 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - if let Some(working_directories) = &a.changes.working_directories { chat.working_directories = Some(working_directories.clone()); } + if let Some(primary_working_directory) = &a.changes.primary_working_directory { + chat.primary_working_directory = Some(primary_working_directory.clone()); + } ReduceOutcome::Applied } StateAction::SessionDefaultChatChanged(a) => { @@ -1949,6 +1952,7 @@ mod tests { activity: None, project: None, working_directories: None, + primary_working_directory: None, annotations: None, lifecycle: SessionLifecycle::Creating, creation_error: None, @@ -1974,6 +1978,7 @@ mod tests { origin: None, interactivity: None, working_directories: None, + primary_working_directory: None, turns: Vec::new(), turns_next_cursor: None, active_turn: None, @@ -2070,6 +2075,7 @@ mod tests { origin: None, interactivity: None, working_directories: None, + primary_working_directory: None, }; let added = StateAction::SessionChatAdded(ahp_types::actions::SessionChatAddedAction { summary: chat.clone(), diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs index 9031a7658..27784fed9 100644 --- a/clients/rust/crates/ahp/tests/hosts.rs +++ b/clients/rust/crates/ahp/tests/hosts.rs @@ -1040,6 +1040,7 @@ fn make_summary(uri: &str, title: &str, modified_at: i64) -> ahp_types::state::S modified_at: modified, project: None, working_directories: None, + primary_working_directory: None, changes: None, annotations: None, meta: None, diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index 23969f940..ac0790d85 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -52,6 +52,7 @@ fn session_state(title: &str, _resource: &str) -> SessionState { activity: None, project: None, working_directories: None, + primary_working_directory: None, annotations: None, lifecycle: SessionLifecycle::Ready, creation_error: None, @@ -355,6 +356,7 @@ fn non_action_event_is_ignored() { modified_at: "1970-01-01T00:00:00.000Z".into(), project: None, working_directories: None, + primary_working_directory: None, changes: None, annotations: None, meta: None, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index d9ac6ee16..2e1b3f564 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1979,6 +1979,9 @@ public struct PartialChatSummary: Codable, Sendable { /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. public var workingDirectories: [String]? + /// The chat's primary working directory. + /// See {@link ChatState.primaryWorkingDirectory} for the full semantics. + public var primaryWorkingDirectory: String? public init( resource: String? = nil, @@ -1988,7 +1991,8 @@ public struct PartialChatSummary: Codable, Sendable { modifiedAt: String? = nil, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, - workingDirectories: [String]? = nil + workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil ) { self.resource = resource self.title = title @@ -1998,6 +2002,7 @@ public struct PartialChatSummary: Codable, Sendable { self.origin = origin self.interactivity = interactivity self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 48029e264..7539388a0 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -357,8 +357,9 @@ public struct CreateSessionParams: Codable, Sendable { /// The working directories the session's agent is granted tool access to. /// A session may span multiple directories; they are equal peers except when /// the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case - /// the first entry is a fixed process root). + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them should be designated the primary via + /// {@link primaryWorkingDirectory}. /// /// A client MUST NOT supply more than one entry unless the agent advertises /// {@link AgentCapabilities.multipleWorkingDirectories}; a server without that @@ -370,6 +371,14 @@ public struct CreateSessionParams: Codable, Sendable { /// Ignored for forked sessions — a fork inherits its working directories /// from the source session identified by `fork`. public var workingDirectories: [String]? + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. A client + /// SHOULD supply this when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + /// reject creation that omits it, or fall back to the first entry of + /// `workingDirectories`. Ignored for forked sessions (a fork inherits the + /// source session's primary). + public var primaryWorkingDirectory: String? /// Fork from an existing session. The new session is populated with content /// from the source session up to and including the specified turn's response. public var fork: SessionForkSource? @@ -399,6 +408,7 @@ public struct CreateSessionParams: Codable, Sendable { channel: String, provider: String? = nil, workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil, fork: SessionForkSource? = nil, config: [String: AnyCodable]? = nil, activeClient: SessionActiveClient? = nil, @@ -407,6 +417,7 @@ public struct CreateSessionParams: Codable, Sendable { self.channel = channel self.provider = provider self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory self.fork = fork self.config = config self.activeClient = activeClient @@ -458,19 +469,29 @@ public struct CreateChatParams: Codable, Sendable { /// A client MUST NOT supply this field unless the agent advertises /// {@link AgentCapabilities.multipleWorkingDirectories}. public var workingDirectories: [String]? + /// The chat's primary working directory — the distinguished root this chat + /// centers on. When set, it MUST be one of the chat's effective working + /// directories ({@link workingDirectories}, or the session's set when that is + /// omitted). A client SHOULD supply this when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat + /// narrows to a subset that excludes the session's primary; when absent, the + /// chat inherits the session's primary. Ignored for forked chats. + public var primaryWorkingDirectory: String? public init( channel: String, chat: String, initialMessage: Message? = nil, source: ChatForkSource? = nil, - workingDirectories: [String]? = nil + workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil ) { self.channel = channel self.chat = chat self.initialMessage = initialMessage self.source = source self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 4ffbb95c7..086a8677a 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -186,12 +186,18 @@ public struct PartialSessionSummary: Codable, Sendable { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - /// entry is then a fixed process root). Individual chats MAY restrict to a - /// subset via {@link ChatSummary.workingDirectories | their own - /// `workingDirectories`}; a chat that sets none operates against this full - /// set. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them is designated the primary (see {@link primaryWorkingDirectory}). + /// Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + /// chat that sets none operates against this full set. public var workingDirectories: [String]? + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. Present + /// when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + /// the agent has no primary (all directories equal peers). + public var primaryWorkingDirectory: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -221,6 +227,7 @@ public struct PartialSessionSummary: Codable, Sendable { case activity case project case workingDirectories + case primaryWorkingDirectory case annotations case resource case createdAt @@ -236,6 +243,7 @@ public struct PartialSessionSummary: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil, annotations: AnnotationsSummary? = nil, resource: String? = nil, createdAt: String? = nil, @@ -249,6 +257,7 @@ public struct PartialSessionSummary: Codable, Sendable { self.activity = activity self.project = project self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 7d41fb0ad..d2ac65037 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -634,8 +634,8 @@ public struct AgentCapabilities: Codable, Sendable { public var multipleChats: MultipleChatsCapability? /// The session's agent can be granted tool access to more than one working /// directory. The directories are treated as equal peers except where the - /// agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} - /// (some backends pin their first directory as a fixed process root). + /// agent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary} + /// (some backends need one directory designated as a primary root). /// /// When absent, clients MUST NOT mutate a session's or chat's working-directory /// set and MUST NOT set more than one entry in @@ -665,22 +665,24 @@ public struct MultipleChatsCapability: Codable, Sendable { } public struct MultipleWorkingDirectoriesCapability: Codable, Sendable { - /// The agent's **first** working directory (index `0` of - /// {@link CreateSessionParams.workingDirectories}) is an immutable primary: - /// it is fixed for the lifetime of the session — clients MUST NOT remove or - /// reorder it. Additional directories after it remain equal peers that can be - /// added and removed freely. + /// The agent requires exactly one of its working directories to be designated + /// the **primary** — a distinguished root the agent centers on (e.g. its + /// process root, the default location for relative paths). When `true`, a + /// client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} + /// (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that + /// narrows the set); a host MAY reject creation that omits it, or fall back to + /// the first entry of `workingDirectories`. The chosen primary is reported on + /// {@link SessionState.primaryWorkingDirectory} / + /// {@link ChatState.primaryWorkingDirectory}. /// - /// Advertised by backends whose agent process is rooted at a single directory - /// that cannot change once the session has started (e.g. the SDK's primary - /// `workingDirectory`). When absent or `false`, all directories are equal - /// peers and any of them may be removed. - public var immutablePrimary: Bool? + /// When absent or `false`, the agent has no primary — all directories are + /// equal peers and clients need not designate one. + public var requiresPrimary: Bool? public init( - immutablePrimary: Bool? = nil + requiresPrimary: Bool? = nil ) { - self.immutablePrimary = immutablePrimary + self.requiresPrimary = requiresPrimary } } @@ -912,6 +914,14 @@ public struct ChatState: Codable, Sendable { /// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to /// update the subset on a running chat. public var workingDirectories: [String]? + /// The chat's primary working directory — the distinguished root this chat + /// centers on. When set, it MUST be one of this chat's effective working + /// directories ({@link workingDirectories}, or the session's set when that is + /// absent). Present when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, + /// the chat inherits {@link SessionState.primaryWorkingDirectory | the + /// session's primary}. + public var primaryWorkingDirectory: String? /// Completed turns public var turns: [Turn] /// Cursor for loading older completed turns into this chat state. @@ -951,6 +961,7 @@ public struct ChatState: Codable, Sendable { case origin case interactivity case workingDirectories + case primaryWorkingDirectory case turns case turnsNextCursor case activeTurn @@ -969,6 +980,7 @@ public struct ChatState: Codable, Sendable { origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil, turns: [Turn], turnsNextCursor: String? = nil, activeTurn: ActiveTurn? = nil, @@ -985,6 +997,7 @@ public struct ChatState: Codable, Sendable { self.origin = origin self.interactivity = interactivity self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory self.turns = turns self.turnsNextCursor = turnsNextCursor self.activeTurn = activeTurn @@ -1017,6 +1030,9 @@ public struct ChatSummary: Codable, Sendable { /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. public var workingDirectories: [String]? + /// The chat's primary working directory. + /// See {@link ChatState.primaryWorkingDirectory} for the full semantics. + public var primaryWorkingDirectory: String? public init( resource: String, @@ -1026,7 +1042,8 @@ public struct ChatSummary: Codable, Sendable { modifiedAt: String, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, - workingDirectories: [String]? = nil + workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil ) { self.resource = resource self.title = title @@ -1036,6 +1053,7 @@ public struct ChatSummary: Codable, Sendable { self.origin = origin self.interactivity = interactivity self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory } } @@ -1054,12 +1072,18 @@ public struct SessionState: Codable, Sendable { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - /// entry is then a fixed process root). Individual chats MAY restrict to a - /// subset via {@link ChatSummary.workingDirectories | their own - /// `workingDirectories`}; a chat that sets none operates against this full - /// set. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them is designated the primary (see {@link primaryWorkingDirectory}). + /// Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + /// chat that sets none operates against this full set. public var workingDirectories: [String]? + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. Present + /// when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + /// the agent has no primary (all directories equal peers). + public var primaryWorkingDirectory: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1145,6 +1169,7 @@ public struct SessionState: Codable, Sendable { case activity case project case workingDirectories + case primaryWorkingDirectory case annotations case lifecycle case creationError @@ -1166,6 +1191,7 @@ public struct SessionState: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil, annotations: AnnotationsSummary? = nil, lifecycle: SessionLifecycle, creationError: ErrorInfo? = nil, @@ -1185,6 +1211,7 @@ public struct SessionState: Codable, Sendable { self.activity = activity self.project = project self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory self.annotations = annotations self.lifecycle = lifecycle self.creationError = creationError @@ -1374,12 +1401,18 @@ public struct SessionSummary: Codable, Sendable { /// maintained by the `session/workingDirectorySet` / /// `session/workingDirectoryRemoved` actions. Directories are equal peers /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - /// entry is then a fixed process root). Individual chats MAY restrict to a - /// subset via {@link ChatSummary.workingDirectories | their own - /// `workingDirectories`}; a chat that sets none operates against this full - /// set. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + /// one of them is designated the primary (see {@link primaryWorkingDirectory}). + /// Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + /// chat that sets none operates against this full set. public var workingDirectories: [String]? + /// The session's primary working directory — the distinguished root the agent + /// centers on. When set, it MUST be one of {@link workingDirectories}. Present + /// when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + /// the agent has no primary (all directories equal peers). + public var primaryWorkingDirectory: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1409,6 +1442,7 @@ public struct SessionSummary: Codable, Sendable { case activity case project case workingDirectories + case primaryWorkingDirectory case annotations case resource case createdAt @@ -1424,6 +1458,7 @@ public struct SessionSummary: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, + primaryWorkingDirectory: String? = nil, annotations: AnnotationsSummary? = nil, resource: String, createdAt: String, @@ -1437,6 +1472,7 @@ public struct SessionSummary: Codable, Sendable { self.activity = activity self.project = project self.workingDirectories = workingDirectories + self.primaryWorkingDirectory = primaryWorkingDirectory self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index dc7027fcd..025e801bc 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -930,6 +930,7 @@ private func mergeChatSummaryChanges(_ summary: inout ChatSummary, changes: Part if let modifiedAt = changes.modifiedAt { summary.modifiedAt = modifiedAt } if let origin = changes.origin { summary.origin = origin } if let workingDirectories = changes.workingDirectories { summary.workingDirectories = workingDirectories } + if let primaryWorkingDirectory = changes.primaryWorkingDirectory { summary.primaryWorkingDirectory = primaryWorkingDirectory } } private func chatSummaryStatus(_ state: ChatState, terminalStatus: SessionStatus? = nil) -> SessionStatus { diff --git a/docs/.changes/20260716-multiroot-sessions.json b/docs/.changes/20260716-multiroot-sessions.json index d2ab878a9..42b32024f 100644 --- a/docs/.changes/20260716-multiroot-sessions.json +++ b/docs/.changes/20260716-multiroot-sessions.json @@ -1,5 +1,5 @@ { "type": "added", - "message": "Multiroot session support: `AgentCapabilities.multipleWorkingDirectories` capability (with `immutablePrimary`), `CreateSessionParams.workingDirectories` and `SessionMetadata.workingDirectories` (equal-peer set, mirrored onto `SessionState`/`SessionSummary`), `CreateChatParams.workingDirectories` and `ChatState`/`ChatSummary.workingDirectories` (a subset of the session's set), and the client-dispatchable `session/workingDirectorySet` / `session/workingDirectoryRemoved` and `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions for mutating those sets.", + "message": "Multiroot session support: `AgentCapabilities.multipleWorkingDirectories` capability (with `requiresPrimary`), `CreateSessionParams.workingDirectories` / `primaryWorkingDirectory` and `SessionMetadata.workingDirectories` / `primaryWorkingDirectory` (equal-peer set plus an optional designated primary, mirrored onto `SessionState`/`SessionSummary`), `CreateChatParams.workingDirectories` / `primaryWorkingDirectory` and `ChatState`/`ChatSummary.workingDirectories` / `primaryWorkingDirectory` (a subset of the session's set with its own optional primary), and the client-dispatchable `session/workingDirectorySet` / `session/workingDirectoryRemoved` and `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions for mutating those sets.", "issues": [337] } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index d36fc920b..05e0ca8bb 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -74,6 +74,7 @@ SessionState { activity?: string project?: ProjectInfo workingDirectories?: URI[] // equal-peer working directories + primaryWorkingDirectory?: URI // designated primary (when requiresPrimary) annotations?: AnnotationsSummary lifecycle: 'creating' | 'ready' | 'creationFailed' @@ -108,6 +109,7 @@ SessionSummary { modifiedAt: string // ISO 8601 project?: ProjectInfo workingDirectories?: URI[] // equal-peer working directories + primaryWorkingDirectory?: URI // designated primary (when requiresPrimary) annotations?: AnnotationsSummary changes?: ChangesSummary } @@ -151,6 +153,7 @@ ChatState { modifiedAt: string origin?: ChatOrigin // how the chat came to exist (user / fork / tool) workingDirectories?: URI[] // subset of session's workingDirectories + primaryWorkingDirectory?: URI // this chat's primary (inherits session's when absent) turns: Turn[] // completed turns turnsNextCursor?: string // page older turns via fetchTurns @@ -566,7 +569,9 @@ The forked session is an independent copy — subsequent changes to either sessi A session can be granted tool access to more than one working directory when the agent advertises the `multipleWorkingDirectories` capability. All directories are -**equal peers** — there is no privileged "primary". +**equal peers** — there is no privileged "primary" — unless the agent advertises +`multipleWorkingDirectories.requiresPrimary`, in which case one directory is +designated the **primary** (see below). ### Creating a multiroot session @@ -580,6 +585,7 @@ createSession({ 'file:///workspace/frontend', 'file:///workspace/backend', ], + primaryWorkingDirectory: 'file:///workspace/frontend', // only when requiresPrimary }); ``` @@ -587,12 +593,16 @@ A client MUST NOT pass more than one entry unless the agent advertises `multipleWorkingDirectories`. Servers without that capability treat only the first entry as the session's working directory and ignore the rest. -The directories are equal peers unless the agent advertises -`multipleWorkingDirectories.immutablePrimary`, in which case the first entry is a -fixed process root that clients MUST NOT remove or reorder. +When the agent advertises `multipleWorkingDirectories.requiresPrimary`, a client +SHOULD supply `primaryWorkingDirectory` (which MUST be one of +`workingDirectories`) to designate the distinguished root the agent centers on. +A host MAY reject a creation that omits it, or fall back to the first entry. The +chosen primary is reported on `SessionState.primaryWorkingDirectory`. When the +agent has no primary, all directories are equal peers and no designation is +needed. -Forked sessions ignore `workingDirectories` — they inherit the working -directories of the source session. +Forked sessions ignore `workingDirectories` / `primaryWorkingDirectory` — they +inherit the working directories (and primary) of the source session. ### Managing directories after creation @@ -602,7 +612,7 @@ mutate it by **dispatching actions**, not by calling commands: | Action | Effect | | --- | --- | | `session/workingDirectorySet` | Adds `directory` to the set (creating it if absent). A no-op when the directory is already present. | -| `session/workingDirectoryRemoved` | Removes `directory` from the set. A no-op when it is not present. There is no atomic backend "remove one" primitive — the host reconfigures its agent to the reduced set. A host MAY decline to apply the removal (e.g. an immutable primary), leaving the set unchanged. | +| `session/workingDirectoryRemoved` | Removes `directory` from the set. A no-op when it is not present. There is no atomic backend "remove one" primitive — the host reconfigures its agent to the reduced set. A host MAY decline to apply the removal (e.g. the current primary of an agent that requires one), leaving the set unchanged. | Both are `@clientDispatchable`. The resulting set is observed on `SessionState.workingDirectories` like any other state — there is no separate @@ -625,13 +635,15 @@ session set. #### Setting at create time Pass `workingDirectories` to `createChat`. Every entry must already exist in -the session's `workingDirectories`: +the session's `workingDirectories`. When the agent `requiresPrimary`, also pass +`primaryWorkingDirectory` if the chat's primary differs from the session's: ```typescript createChat({ channel: 'ahp-session:/', chat: 'ahp-chat:/', workingDirectories: ['file:///workspace/frontend'], // subset + primaryWorkingDirectory: 'file:///workspace/frontend', // optional; inherits session's when omitted }); ``` diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index f89f6afd0..8d977fe22 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -170,9 +170,11 @@ throughout — no juggling three disconnected sessions. root), not arbitrary path patterns within it. - **No mandatory primary.** All directories are peers by default. A backend that - *must* pin its first directory (a fixed process root) advertises - `immutablePrimary` on the capability; clients then keep index `0` fixed. The - protocol has no other notion of a "main" directory. + *needs* one distinguished root advertises `requiresPrimary` on the capability; + the client then designates it explicitly via `primaryWorkingDirectory` (on + `createSession`/`createChat`, reported back in state). The protocol has no + other notion of a "main" directory, and — deliberately — primacy is **not** + tied to array position. - **It is not multi-root *chats as sub-sessions*.** A chat narrowing to a subset is still one thread under one session's trust and identity. Independent agents @@ -230,12 +232,12 @@ as today. | Symbol | File | Kind | | --- | --- | --- | | `AgentCapabilities.multipleWorkingDirectories?` | `channels-root/state.ts` | added (capability) | -| `MultipleWorkingDirectoriesCapability` (`immutablePrimary?`) | `channels-root/state.ts` | added (type) | -| `CreateSessionParams.workingDirectories?` | `channels-session/commands.ts` | added | -| `SessionMetadata.workingDirectories?` (→ `SessionState`, `SessionSummary`) | `channels-session/state.ts` | added | +| `MultipleWorkingDirectoriesCapability` (`requiresPrimary?`) | `channels-root/state.ts` | added (type) | +| `CreateSessionParams.workingDirectories?` / `primaryWorkingDirectory?` | `channels-session/commands.ts` | added | +| `SessionMetadata.workingDirectories?` / `primaryWorkingDirectory?` (→ `SessionState`, `SessionSummary`) | `channels-session/state.ts` | added | | `session/workingDirectorySet` / `session/workingDirectoryRemoved` actions | `channels-session/actions.ts` | added (action) | -| `ChatState.workingDirectories?` / `ChatSummary.workingDirectories?` | `channels-chat/state.ts` | added | -| `CreateChatParams.workingDirectories?` | `channels-chat/commands.ts` | added | +| `ChatState.workingDirectories?` / `primaryWorkingDirectory?` (& `ChatSummary`) | `channels-chat/state.ts` | added | +| `CreateChatParams.workingDirectories?` / `primaryWorkingDirectory?` | `channels-chat/commands.ts` | added | | `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions | `channels-chat/actions.ts` | added (action) | | 4 `ActionType` entries + `ACTION_INTRODUCED_IN` (`0.7.0`) | `common/actions.ts`, `version/registry.ts` | added | @@ -262,8 +264,8 @@ interface AgentCapabilities { multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability; } interface MultipleWorkingDirectoriesCapability { - /** First directory is a fixed process root; clients MUST NOT remove/reorder it. */ - immutablePrimary?: boolean; + /** The agent needs one directory designated as its primary root. */ + requiresPrimary?: boolean; } // ── Session create — channels-session/commands.ts ──────────────────────── @@ -271,12 +273,15 @@ interface CreateSessionParams extends BaseParams { // …existing… /** The session's equal-peer working directories. */ workingDirectories?: URI[]; + /** Designated primary (⊆ workingDirectories); supply when requiresPrimary. */ + primaryWorkingDirectory?: URI; } // ── Session state — channels-session/state.ts (→ SessionState/SessionSummary) interface SessionMetadata { // …existing… workingDirectories?: URI[]; + primaryWorkingDirectory?: URI; // host's authoritative "which is primary" } // ── Session runtime mutation — channels-session/actions.ts ──────────────── @@ -295,10 +300,12 @@ interface ChatState /* and ChatSummary */ { // …existing… /** The chat's subset — every entry MUST be one of the session's dirs. */ workingDirectories?: URI[]; + primaryWorkingDirectory?: URI; // chat's primary; inherits session's when absent } interface CreateChatParams extends BaseParams { // …existing… workingDirectories?: URI[]; // subset ⊆ session; absent → whole session set + primaryWorkingDirectory?: URI; // ⊆ chat's dirs; inherits session's when absent } // channel = chat URI. Gated by multipleWorkingDirectories. @clientDispatchable. interface ChatWorkingDirectorySetAction { @@ -330,17 +337,22 @@ interface ChatWorkingDirectoryRemovedAction { handshake. - Removal actions are idempotent and modelled as *reconfigure-to-the-reduced-set*; a host MAY decline to apply a removal (e.g. - an `immutablePrimary` directory), leaving the set unchanged. + the current primary directory of an agent that `requiresPrimary`), leaving the + set unchanged. --- ## 9. Design decisions & resolved questions -- **No mandatory primary.** *Resolved:* the set is equal peers. Rejected a - "primary + additional" split because it re-introduces the confusion the user - called out ("what's the relation between primary and secondary?"). Backends - that genuinely pin a fixed process root opt in via - `MultipleWorkingDirectoriesCapability.immutablePrimary` (index `0` fixed). +- **No mandatory primary; when needed, it's explicit.** *Resolved:* the set is + equal peers by default. Rejected a "primary + additional" split because it + re-introduces the confusion the user called out ("what's the relation between + primary and secondary?"). An agent that genuinely needs one distinguished root + advertises `MultipleWorkingDirectoriesCapability.requiresPrimary`; the client + then designates it **explicitly** via `primaryWorkingDirectory` on + `createSession` / `createChat` (reported back in state). Deliberately *not* + positional — "primary" is never inferred from array index, so removals and + reordering stay safe. - **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* removing these fields is a breaking change, so the feature targets a MINOR @@ -375,7 +387,8 @@ interface ChatWorkingDirectoryRemovedAction { - **Removal semantics.** *Resolved:* no single-remove primitive is assumed; the `*/workingDirectoryRemoved` action reduces to the reduced set, so it is - idempotent and safe to retry. A host MAY decline (e.g. an immutable primary). + idempotent and safe to retry. A host MAY decline (e.g. the current primary of + an agent that `requiresPrimary`). --- diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 85d016fd2..2a9f0ccdf 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -239,6 +239,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." @@ -427,7 +431,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. the current primary directory of an agent\nthat {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a\nprimary}); it then leaves the set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" @@ -2717,7 +2721,7 @@ }, "multipleWorkingDirectories": { "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}\n(some backends need one directory designated as a primary root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -2735,9 +2739,9 @@ "type": "object", "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { - "immutablePrimary": { + "requiresPrimary": { "type": "boolean", - "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -2860,7 +2864,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2902,7 +2910,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3206,7 +3218,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -4493,6 +4509,10 @@ }, "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + }, "turns": { "type": "array", "items": { @@ -4575,6 +4595,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 53ef3da19..2619dc00d 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -967,7 +967,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them should be designated the primary via\n{@link primaryWorkingDirectory}.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. A client\nSHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's primary)." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -1140,6 +1144,10 @@ "$ref": "#/$defs/URI" }, "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of the chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nomitted). A client SHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat\nnarrows to a subset that excludes the session's primary; when absent, the\nchat inherits the session's primary. Ignored for forked chats." } }, "required": [ @@ -1856,7 +1864,7 @@ }, "multipleWorkingDirectories": { "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}\n(some backends need one directory designated as a primary root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -1874,9 +1882,9 @@ "type": "object", "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { - "immutablePrimary": { + "requiresPrimary": { "type": "boolean", - "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -1999,7 +2007,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2041,7 +2053,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2345,7 +2361,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3632,6 +3652,10 @@ }, "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + }, "turns": { "type": "array", "items": { @@ -3714,6 +3738,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "required": [ @@ -6368,6 +6396,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." @@ -6556,7 +6588,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. the current primary directory of an agent\nthat {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a\nprimary}); it then leaves the set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" diff --git a/schema/errors.schema.json b/schema/errors.schema.json index e12aefaab..c725289df 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -630,7 +630,7 @@ }, "multipleWorkingDirectories": { "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}\n(some backends need one directory designated as a primary root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -648,9 +648,9 @@ "type": "object", "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { - "immutablePrimary": { + "requiresPrimary": { "type": "boolean", - "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -773,7 +773,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -815,7 +819,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1119,7 +1127,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2406,6 +2418,10 @@ }, "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + }, "turns": { "type": "array", "items": { @@ -2488,6 +2504,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "required": [ @@ -5870,7 +5890,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them should be designated the primary via\n{@link primaryWorkingDirectory}.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. A client\nSHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's primary)." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -6043,6 +6067,10 @@ "$ref": "#/$defs/URI" }, "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (`source`) inherit the source chat's\n`workingDirectories`; this field is ignored for forked chats.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of the chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nomitted). A client SHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat\nnarrows to a subset that excludes the session's primary; when absent, the\nchat inherits the session's primary. Ignored for forked chats." } }, "required": [ @@ -7343,6 +7371,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." @@ -7460,7 +7492,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. an immutable primary directory, see\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves\nthe set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. the current primary directory of an agent\nthat {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a\nprimary}); it then leaves the set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 88983529b..22d92c0ad 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -103,7 +103,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -793,7 +797,7 @@ }, "multipleWorkingDirectories": { "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}\n(some backends need one directory designated as a primary root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -811,9 +815,9 @@ "type": "object", "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { - "immutablePrimary": { + "requiresPrimary": { "type": "boolean", - "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -936,7 +940,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -978,7 +986,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1282,7 +1294,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2569,6 +2585,10 @@ }, "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + }, "turns": { "type": "array", "items": { @@ -2651,6 +2671,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index d5c259572..25bacc77d 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -541,7 +541,7 @@ }, "multipleWorkingDirectories": { "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}\n(some backends need one directory designated as a primary root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." } } }, @@ -559,9 +559,9 @@ "type": "object", "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { - "immutablePrimary": { + "requiresPrimary": { "type": "boolean", - "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -684,7 +684,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -726,7 +730,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1030,7 +1038,11 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2317,6 +2329,10 @@ }, "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + }, "turns": { "type": "array", "items": { @@ -2399,6 +2415,10 @@ "$ref": "#/$defs/URI" }, "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + }, + "primaryWorkingDirectory": { + "$ref": "#/$defs/URI", + "description": "The chat's primary working directory.\nSee {@link ChatState.primaryWorkingDirectory} for the full semantics." } }, "required": [ diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 377f4df3c..cf5f61762 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -49,6 +49,16 @@ export interface CreateChatParams extends BaseParams { * {@link AgentCapabilities.multipleWorkingDirectories}. */ workingDirectories?: URI[]; + /** + * The chat's primary working directory — the distinguished root this chat + * centers on. When set, it MUST be one of the chat's effective working + * directories ({@link workingDirectories}, or the session's set when that is + * omitted). A client SHOULD supply this when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat + * narrows to a subset that excludes the session's primary; when absent, the + * chat inherits the session's primary. Ignored for forked chats. + */ + primaryWorkingDirectory?: URI; } // ─── disposeChat ───────────────────────────────────────────────────────────── diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 1cec32192..b423f842e 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -71,6 +71,16 @@ export interface ChatState { * update the subset on a running chat. */ workingDirectories?: URI[]; + /** + * The chat's primary working directory — the distinguished root this chat + * centers on. When set, it MUST be one of this chat's effective working + * directories ({@link workingDirectories}, or the session's set when that is + * absent). Present when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, + * the chat inherits {@link SessionState.primaryWorkingDirectory | the + * session's primary}. + */ + primaryWorkingDirectory?: URI; // ── Conversation contents ────────────────────────────────────────── /** Completed turns */ @@ -143,6 +153,11 @@ export interface ChatSummary { * See {@link ChatState.workingDirectories} for the full semantics. */ workingDirectories?: URI[]; + /** + * The chat's primary working directory. + * See {@link ChatState.primaryWorkingDirectory} for the full semantics. + */ + primaryWorkingDirectory?: URI; } /** diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index 49d71ee84..3cd254396 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -113,8 +113,8 @@ export interface AgentCapabilities { /** * The session's agent can be granted tool access to more than one working * directory. The directories are treated as equal peers except where the - * agent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary} - * (some backends pin their first directory as a fixed process root). + * agent advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary} + * (some backends need one directory designated as a primary root). * * When absent, clients MUST NOT mutate a session's or chat's working-directory * set and MUST NOT set more than one entry in @@ -144,18 +144,20 @@ export interface MultipleChatsCapability { */ export interface MultipleWorkingDirectoriesCapability { /** - * The agent's **first** working directory (index `0` of - * {@link CreateSessionParams.workingDirectories}) is an immutable primary: - * it is fixed for the lifetime of the session — clients MUST NOT remove or - * reorder it. Additional directories after it remain equal peers that can be - * added and removed freely. + * The agent requires exactly one of its working directories to be designated + * the **primary** — a distinguished root the agent centers on (e.g. its + * process root, the default location for relative paths). When `true`, a + * client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} + * (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that + * narrows the set); a host MAY reject creation that omits it, or fall back to + * the first entry of `workingDirectories`. The chosen primary is reported on + * {@link SessionState.primaryWorkingDirectory} / + * {@link ChatState.primaryWorkingDirectory}. * - * Advertised by backends whose agent process is rooted at a single directory - * that cannot change once the session has started (e.g. the SDK's primary - * `workingDirectory`). When absent or `false`, all directories are equal - * peers and any of them may be removed. + * When absent or `false`, the agent has no primary — all directories are + * equal peers and clients need not designate one. */ - immutablePrimary?: boolean; + requiresPrimary?: boolean; } /** diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 5ba4ef058..ccb1377e7 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -283,9 +283,9 @@ export interface SessionWorkingDirectorySetAction { * Removes `directory` from the set; a no-op when it is not present. There is no * atomic backend "remove one" primitive — a host reconfigures its agent to the * reduced set — so this action is safe to model as idempotent. A host MAY - * decline to apply the removal (e.g. an immutable primary directory, see - * {@link MultipleWorkingDirectoriesCapability.immutablePrimary}); it then leaves - * the set unchanged. + * decline to apply the removal (e.g. the current primary directory of an agent + * that {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a + * primary}); it then leaves the set unchanged. * * @category Session Actions * @version 1 diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index 7b07a61ae..fa63165da 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -70,8 +70,9 @@ export interface CreateSessionParams extends BaseParams { * The working directories the session's agent is granted tool access to. * A session may span multiple directories; they are equal peers except when * the agent advertises - * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case - * the first entry is a fixed process root). + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + * one of them should be designated the primary via + * {@link primaryWorkingDirectory}. * * A client MUST NOT supply more than one entry unless the agent advertises * {@link AgentCapabilities.multipleWorkingDirectories}; a server without that @@ -84,6 +85,16 @@ export interface CreateSessionParams extends BaseParams { * from the source session identified by `fork`. */ workingDirectories?: URI[]; + /** + * The session's primary working directory — the distinguished root the agent + * centers on. When set, it MUST be one of {@link workingDirectories}. A client + * SHOULD supply this when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + * reject creation that omits it, or fall back to the first entry of + * `workingDirectories`. Ignored for forked sessions (a fork inherits the + * source session's primary). + */ + primaryWorkingDirectory?: URI; /** * Fork from an existing session. The new session is populated with content * from the source session up to and including the specified turn's response. diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 6a1832a9d..32b44e809 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -88,13 +88,21 @@ export interface SessionMetadata { * maintained by the `session/workingDirectorySet` / * `session/workingDirectoryRemoved` actions. Directories are equal peers * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first - * entry is then a fixed process root). Individual chats MAY restrict to a - * subset via {@link ChatSummary.workingDirectories | their own - * `workingDirectories`}; a chat that sets none operates against this full - * set. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case + * one of them is designated the primary (see {@link primaryWorkingDirectory}). + * Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a + * chat that sets none operates against this full set. */ workingDirectories?: URI[]; + /** + * The session's primary working directory — the distinguished root the agent + * centers on. When set, it MUST be one of {@link workingDirectories}. Present + * when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when + * the agent has no primary (all directories equal peers). + */ + primaryWorkingDirectory?: URI; /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render From ea279d99a4f38d73ad394bede3f95d4775eb7c1b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 21 Jul 2026 23:12:27 -0700 Subject: [PATCH 14/15] spec: make primaryWorkingDirectory a per-chat, read-only, creation-fixed field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finalized design for "primary working directory": - The SESSION has no primary. Dropped primaryWorkingDirectory from SessionMetadata (SessionState/SessionSummary). The session is purely an equal-peer directory set. - Primary is a PER-CHAT notion: ChatState.primaryWorkingDirectory (mirrored on ChatSummary) is user-visible read-only state, fixed at chat creation. There is no action to change it and it does not participate in session/chatUpdated — "in state" (so late subscribers can read it) but immutable (no action). - Set via CreateChatParams.primaryWorkingDirectory; CreateSessionParams. primaryWorkingDirectory now seeds the session's default chat. - Reworded the requiresPrimary capability accordingly (per-chat, fixed at creation) and the session/workingDirectoryRemoved decline note. - Removed the primaryWorkingDirectory handling from the client chatUpdated partial-summary merges (immutable → not a partial-update field) and dropped it from the Session rust struct literals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahp/reducers.go | 3 - clients/go/ahptypes/actions.generated.go | 6 +- clients/go/ahptypes/commands.generated.go | 26 ++++--- .../go/ahptypes/notifications.generated.go | 19 ++--- clients/go/ahptypes/state.generated.go | 71 ++++++++--------- .../microsoft/agenthostprotocol/Reducers.kt | 1 - .../generated/Commands.generated.kt | 26 ++++--- .../generated/Notifications.generated.kt | 21 ++--- .../generated/State.generated.kt | 75 ++++++++---------- clients/rust/crates/ahp-types/src/actions.rs | 6 +- clients/rust/crates/ahp-types/src/commands.rs | 26 ++++--- .../crates/ahp-types/src/notifications.rs | 20 ++--- clients/rust/crates/ahp-types/src/state.rs | 73 ++++++++---------- clients/rust/crates/ahp/src/reducers.rs | 4 - clients/rust/crates/ahp/tests/hosts.rs | 1 - .../ahp/tests/multi_host_state_mirror.rs | 2 - .../Generated/Commands.generated.swift | 26 ++++--- .../Generated/Notifications.generated.swift | 22 ++---- .../Generated/State.generated.swift | 77 ++++++++----------- .../Sources/AgentHostProtocol/Reducers.swift | 1 - .../.changes/20260716-multiroot-sessions.json | 2 +- docs/guide/state-model.md | 43 ++++++----- docs/proposals/multiroot-sessions.md | 56 ++++++++------ schema/actions.schema.json | 24 ++---- schema/commands.schema.json | 28 ++----- schema/errors.schema.json | 28 ++----- schema/notifications.schema.json | 28 ++----- schema/state.schema.json | 22 ++---- types/channels-chat/commands.ts | 12 +-- types/channels-chat/state.ts | 15 ++-- types/channels-root/state.ts | 18 ++--- types/channels-session/actions.ts | 6 +- types/channels-session/commands.ts | 14 ++-- types/channels-session/state.ts | 21 ++--- 34 files changed, 346 insertions(+), 477 deletions(-) diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 364801d58..43f659a4b 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -770,9 +770,6 @@ func mergeChatSummaryPartial(summary *ahptypes.ChatSummary, changes ahptypes.Par if changes.WorkingDirectories != nil { summary.WorkingDirectories = changes.WorkingDirectories } - if changes.PrimaryWorkingDirectory != nil { - summary.PrimaryWorkingDirectory = changes.PrimaryWorkingDirectory - } } // ─── Session Reducer ─────────────────────────────────────────────────── diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index b4ce2edb8..946b45746 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -898,9 +898,9 @@ type SessionWorkingDirectorySetAction struct { // Removes `directory` from the set; a no-op when it is not present. There is no // atomic backend "remove one" primitive — a host reconfigures its agent to the // reduced set — so this action is safe to model as idempotent. A host MAY -// decline to apply the removal (e.g. the current primary directory of an agent -// that {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a -// primary}); it then leaves the set unchanged. +// decline to apply the removal (e.g. a directory still designated as some +// chat's {@link ChatState.primaryWorkingDirectory | primary}); it then leaves +// the set unchanged. type SessionWorkingDirectoryRemovedAction struct { Type ActionType `json:"type"` // The working directory to revoke the session's agent tool access to. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 3f79a05da..2f7369085 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -322,13 +322,15 @@ type CreateSessionParams struct { // Ignored for forked sessions — a fork inherits its working directories // from the source session identified by `fork`. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // The session's primary working directory — the distinguished root the agent - // centers on. When set, it MUST be one of {@link workingDirectories}. A client - // SHOULD supply this when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY - // reject creation that omits it, or fall back to the first entry of + // The primary working directory for the session's **default chat** — the + // distinguished root that chat is centered on (see + // {@link ChatState.primaryWorkingDirectory}). A session has no primary of its + // own; this seeds the default chat's primary. When set, it MUST be one of + // {@link workingDirectories}. A client SHOULD supply this when the agent + // advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a + // host MAY reject creation that omits it, or fall back to the first entry of // `workingDirectories`. Ignored for forked sessions (a fork inherits the - // source session's primary). + // source session's chats and their primaries). PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Fork from an existing session. The new session is populated with content // from the source session up to and including the specified turn's response. @@ -391,13 +393,15 @@ type CreateChatParams struct { // A client MUST NOT supply this field unless the agent advertises // {@link AgentCapabilities.multipleWorkingDirectories}. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // The chat's primary working directory — the distinguished root this chat - // centers on. When set, it MUST be one of the chat's effective working + // The chat's primary working directory — the distinguished root this chat is + // centered on. When set, it MUST be one of the chat's effective working // directories ({@link workingDirectories}, or the session's set when that is // omitted). A client SHOULD supply this when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat - // narrows to a subset that excludes the session's primary; when absent, the - // chat inherits the session's primary. Ignored for forked chats. + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + // reject creation that omits it, or fall back to the first of the chat's + // directories. Fixed at creation and reported (read-only) on + // {@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork + // inherits the source chat's primary). PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` } diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 31f7508b2..ed5539968 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -219,20 +219,13 @@ type PartialSessionSummary struct { Project *ProjectInfo `json:"project,omitempty"` // The working directories the session's agent has tool access to, as // maintained by the `session/workingDirectorySet` / - // `session/workingDirectoryRemoved` actions. Directories are equal peers - // except when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - // one of them is designated the primary (see {@link primaryWorkingDirectory}). - // Individual chats MAY restrict to a subset via - // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - // chat that sets none operates against this full set. + // `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + // the session has no primary. Individual chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + // designate one of their own directories as primary (see + // {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + // operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // The session's primary working directory — the distinguished root the agent - // centers on. When set, it MUST be one of {@link workingDirectories}. Present - // when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - // the agent has no primary (all directories equal peers). - PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index f7c3b755a..3f5ba0412 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -605,15 +605,15 @@ type MultipleChatsCapability struct { // Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. type MultipleWorkingDirectoriesCapability struct { - // The agent requires exactly one of its working directories to be designated - // the **primary** — a distinguished root the agent centers on (e.g. its - // process root, the default location for relative paths). When `true`, a - // client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} - // (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that - // narrows the set); a host MAY reject creation that omits it, or fall back to - // the first entry of `workingDirectories`. The chosen primary is reported on - // {@link SessionState.primaryWorkingDirectory} / - // {@link ChatState.primaryWorkingDirectory}. + // The agent requires each chat to designate one of its working directories as + // the **primary** — a distinguished root the chat is centered on (e.g. the + // agent's process root for that chat, the default location for relative + // paths). Primary is a **per-chat** notion, fixed at chat creation. When + // `true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory} + // (and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the + // session's default chat); a host MAY reject creation that omits it, or fall + // back to the first entry of the chat's working directories. The chosen + // primary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}. // // When absent or `false`, the agent has no primary — all directories are // equal peers and clients need not designate one. @@ -742,20 +742,13 @@ type SessionState struct { Project *ProjectInfo `json:"project,omitempty"` // The working directories the session's agent has tool access to, as // maintained by the `session/workingDirectorySet` / - // `session/workingDirectoryRemoved` actions. Directories are equal peers - // except when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - // one of them is designated the primary (see {@link primaryWorkingDirectory}). - // Individual chats MAY restrict to a subset via - // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - // chat that sets none operates against this full set. + // `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + // the session has no primary. Individual chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + // designate one of their own directories as primary (see + // {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + // operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // The session's primary working directory — the distinguished root the agent - // centers on. When set, it MUST be one of {@link workingDirectories}. Present - // when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - // the agent has no primary (all directories equal peers). - PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -1014,20 +1007,13 @@ type SessionSummary struct { Project *ProjectInfo `json:"project,omitempty"` // The working directories the session's agent has tool access to, as // maintained by the `session/workingDirectorySet` / - // `session/workingDirectoryRemoved` actions. Directories are equal peers - // except when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - // one of them is designated the primary (see {@link primaryWorkingDirectory}). - // Individual chats MAY restrict to a subset via - // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - // chat that sets none operates against this full set. + // `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + // the session has no primary. Individual chats MAY restrict to a subset via + // {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + // designate one of their own directories as primary (see + // {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + // operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // The session's primary working directory — the distinguished root the agent - // centers on. When set, it MUST be one of {@link workingDirectories}. Present - // when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - // the agent has no primary (all directories equal peers). - PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -1106,13 +1092,18 @@ type ChatState struct { // Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to // update the subset on a running chat. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // The chat's primary working directory — the distinguished root this chat - // centers on. When set, it MUST be one of this chat's effective working + // The chat's primary working directory — the distinguished root this chat is + // centered on (e.g. the agent's process root for this chat, the default + // location for relative paths). MUST be one of this chat's effective working // directories ({@link workingDirectories}, or the session's set when that is // absent). Present when the agent advertises - // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, - // the chat inherits {@link SessionState.primaryWorkingDirectory | the - // session's primary}. + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}. + // + // **Read-only and fixed at creation.** It is set from + // {@link CreateChatParams.primaryWorkingDirectory} (or, for the session's + // default chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does + // not change over the chat's lifetime — there is no action to mutate it, and + // it does not participate in `session/chatUpdated`. PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` // Completed turns Turns []Turn `json:"turns"` diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index 95a751e77..e681f91fb 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -546,7 +546,6 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat modifiedAt = c.modifiedAt ?: prior.modifiedAt, origin = c.origin ?: prior.origin, workingDirectories = c.workingDirectories ?: prior.workingDirectories, - primaryWorkingDirectory = c.primaryWorkingDirectory ?: prior.primaryWorkingDirectory, ) val updated = state.chats.toMutableList() updated[idx] = updatedSummary diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 5f0431cfd..93cf8f73c 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -388,13 +388,15 @@ data class CreateSessionParams( */ val workingDirectories: List? = null, /** - * The session's primary working directory — the distinguished root the agent - * centers on. When set, it MUST be one of {@link workingDirectories}. A client - * SHOULD supply this when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY - * reject creation that omits it, or fall back to the first entry of + * The primary working directory for the session's **default chat** — the + * distinguished root that chat is centered on (see + * {@link ChatState.primaryWorkingDirectory}). A session has no primary of its + * own; this seeds the default chat's primary. When set, it MUST be one of + * {@link workingDirectories}. A client SHOULD supply this when the agent + * advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a + * host MAY reject creation that omits it, or fall back to the first entry of * `workingDirectories`. Ignored for forked sessions (a fork inherits the - * source session's primary). + * source session's chats and their primaries). */ val primaryWorkingDirectory: String? = null, /** @@ -481,13 +483,15 @@ data class CreateChatParams( */ val workingDirectories: List? = null, /** - * The chat's primary working directory — the distinguished root this chat - * centers on. When set, it MUST be one of the chat's effective working + * The chat's primary working directory — the distinguished root this chat is + * centered on. When set, it MUST be one of the chat's effective working * directories ({@link workingDirectories}, or the session's set when that is * omitted). A client SHOULD supply this when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat - * narrows to a subset that excludes the session's primary; when absent, the - * chat inherits the session's primary. Ignored for forked chats. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + * reject creation that omits it, or fall back to the first of the chat's + * directories. Fixed at creation and reported (read-only) on + * {@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork + * inherits the source chat's primary). */ val primaryWorkingDirectory: String? = null ) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 09b2d3759..392401e94 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -198,23 +198,14 @@ data class PartialSessionSummary( /** * The working directories the session's agent has tool access to, as * maintained by the `session/workingDirectorySet` / - * `session/workingDirectoryRemoved` actions. Directories are equal peers - * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - * one of them is designated the primary (see {@link primaryWorkingDirectory}). - * Individual chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - * chat that sets none operates against this full set. + * `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + * the session has no primary. Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + * designate one of their own directories as primary (see + * {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + * operates against this full set. */ val workingDirectories: List? = null, - /** - * The session's primary working directory — the distinguished root the agent - * centers on. When set, it MUST be one of {@link workingDirectories}. Present - * when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - * the agent has no primary (all directories equal peers). - */ - val primaryWorkingDirectory: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 2b39c6514..1607814e3 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -979,15 +979,15 @@ data class MultipleChatsCapability( @Serializable data class MultipleWorkingDirectoriesCapability( /** - * The agent requires exactly one of its working directories to be designated - * the **primary** — a distinguished root the agent centers on (e.g. its - * process root, the default location for relative paths). When `true`, a - * client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} - * (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that - * narrows the set); a host MAY reject creation that omits it, or fall back to - * the first entry of `workingDirectories`. The chosen primary is reported on - * {@link SessionState.primaryWorkingDirectory} / - * {@link ChatState.primaryWorkingDirectory}. + * The agent requires each chat to designate one of its working directories as + * the **primary** — a distinguished root the chat is centered on (e.g. the + * agent's process root for that chat, the default location for relative + * paths). Primary is a **per-chat** notion, fixed at chat creation. When + * `true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory} + * (and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the + * session's default chat); a host MAY reject creation that omits it, or fall + * back to the first entry of the chat's working directories. The chosen + * primary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}. * * When absent or `false`, the agent has no primary — all directories are * equal peers and clients need not designate one. @@ -1196,13 +1196,18 @@ data class ChatState( */ val workingDirectories: List? = null, /** - * The chat's primary working directory — the distinguished root this chat - * centers on. When set, it MUST be one of this chat's effective working + * The chat's primary working directory — the distinguished root this chat is + * centered on (e.g. the agent's process root for this chat, the default + * location for relative paths). MUST be one of this chat's effective working * directories ({@link workingDirectories}, or the session's set when that is * absent). Present when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, - * the chat inherits {@link SessionState.primaryWorkingDirectory | the - * session's primary}. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}. + * + * **Read-only and fixed at creation.** It is set from + * {@link CreateChatParams.primaryWorkingDirectory} (or, for the session's + * default chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does + * not change over the chat's lifetime — there is no action to mutate it, and + * it does not participate in `session/chatUpdated`. */ val primaryWorkingDirectory: String? = null, /** @@ -1322,23 +1327,14 @@ data class SessionState( /** * The working directories the session's agent has tool access to, as * maintained by the `session/workingDirectorySet` / - * `session/workingDirectoryRemoved` actions. Directories are equal peers - * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - * one of them is designated the primary (see {@link primaryWorkingDirectory}). - * Individual chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - * chat that sets none operates against this full set. + * `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + * the session has no primary. Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + * designate one of their own directories as primary (see + * {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + * operates against this full set. */ val workingDirectories: List? = null, - /** - * The session's primary working directory — the distinguished root the agent - * centers on. When set, it MUST be one of {@link workingDirectories}. Present - * when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - * the agent has no primary (all directories equal peers). - */ - val primaryWorkingDirectory: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1604,23 +1600,14 @@ data class SessionSummary( /** * The working directories the session's agent has tool access to, as * maintained by the `session/workingDirectorySet` / - * `session/workingDirectoryRemoved` actions. Directories are equal peers - * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - * one of them is designated the primary (see {@link primaryWorkingDirectory}). - * Individual chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - * chat that sets none operates against this full set. + * `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + * the session has no primary. Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + * designate one of their own directories as primary (see + * {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + * operates against this full set. */ val workingDirectories: List? = null, - /** - * The session's primary working directory — the distinguished root the agent - * centers on. When set, it MUST be one of {@link workingDirectories}. Present - * when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - * the agent has no primary (all directories equal peers). - */ - val primaryWorkingDirectory: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 776ecf1a5..aa2ad448d 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -954,9 +954,9 @@ pub struct SessionWorkingDirectorySetAction { /// Removes `directory` from the set; a no-op when it is not present. There is no /// atomic backend "remove one" primitive — a host reconfigures its agent to the /// reduced set — so this action is safe to model as idempotent. A host MAY -/// decline to apply the removal (e.g. the current primary directory of an agent -/// that {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a -/// primary}); it then leaves the set unchanged. +/// decline to apply the removal (e.g. a directory still designated as some +/// chat's {@link ChatState.primaryWorkingDirectory | primary}); it then leaves +/// the set unchanged. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionWorkingDirectoryRemovedAction { diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index f49b30fcd..1afab4bde 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -409,13 +409,15 @@ pub struct CreateSessionParams { /// from the source session identified by `fork`. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. A client - /// SHOULD supply this when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY - /// reject creation that omits it, or fall back to the first entry of + /// The primary working directory for the session's **default chat** — the + /// distinguished root that chat is centered on (see + /// {@link ChatState.primaryWorkingDirectory}). A session has no primary of its + /// own; this seeds the default chat's primary. When set, it MUST be one of + /// {@link workingDirectories}. A client SHOULD supply this when the agent + /// advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a + /// host MAY reject creation that omits it, or fall back to the first entry of /// `workingDirectories`. Ignored for forked sessions (a fork inherits the - /// source session's primary). + /// source session's chats and their primaries). #[serde(default, skip_serializing_if = "Option::is_none")] pub primary_working_directory: Option, /// Fork from an existing session. The new session is populated with content @@ -492,13 +494,15 @@ pub struct CreateChatParams { /// {@link AgentCapabilities.multipleWorkingDirectories}. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// The chat's primary working directory — the distinguished root this chat - /// centers on. When set, it MUST be one of the chat's effective working + /// The chat's primary working directory — the distinguished root this chat is + /// centered on. When set, it MUST be one of the chat's effective working /// directories ({@link workingDirectories}, or the session's set when that is /// omitted). A client SHOULD supply this when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat - /// narrows to a subset that excludes the session's primary; when absent, the - /// chat inherits the session's primary. Ignored for forked chats. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + /// reject creation that omits it, or fall back to the first of the chat's + /// directories. Fixed at creation and reported (read-only) on + /// {@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork + /// inherits the source chat's primary). #[serde(default, skip_serializing_if = "Option::is_none")] pub primary_working_directory: Option, } diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index afa106164..e56151bfc 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -250,22 +250,14 @@ pub struct PartialSessionSummary { pub project: Option, /// The working directories the session's agent has tool access to, as /// maintained by the `session/workingDirectorySet` / - /// `session/workingDirectoryRemoved` actions. Directories are equal peers - /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - /// one of them is designated the primary (see {@link primaryWorkingDirectory}). - /// Individual chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - /// chat that sets none operates against this full set. + /// `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + /// the session has no primary. Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + /// designate one of their own directories as primary (see + /// {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + /// operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. Present - /// when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - /// the agent has no primary (all directories equal peers). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub primary_working_directory: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 0c6b5ae42..18ca75394 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -830,15 +830,15 @@ pub struct MultipleChatsCapability { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct MultipleWorkingDirectoriesCapability { - /// The agent requires exactly one of its working directories to be designated - /// the **primary** — a distinguished root the agent centers on (e.g. its - /// process root, the default location for relative paths). When `true`, a - /// client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} - /// (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that - /// narrows the set); a host MAY reject creation that omits it, or fall back to - /// the first entry of `workingDirectories`. The chosen primary is reported on - /// {@link SessionState.primaryWorkingDirectory} / - /// {@link ChatState.primaryWorkingDirectory}. + /// The agent requires each chat to designate one of its working directories as + /// the **primary** — a distinguished root the chat is centered on (e.g. the + /// agent's process root for that chat, the default location for relative + /// paths). Primary is a **per-chat** notion, fixed at chat creation. When + /// `true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory} + /// (and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the + /// session's default chat); a host MAY reject creation that omits it, or fall + /// back to the first entry of the chat's working directories. The chosen + /// primary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}. /// /// When absent or `false`, the agent has no primary — all directories are /// equal peers and clients need not designate one. @@ -1039,13 +1039,18 @@ pub struct ChatState { /// update the subset on a running chat. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// The chat's primary working directory — the distinguished root this chat - /// centers on. When set, it MUST be one of this chat's effective working + /// The chat's primary working directory — the distinguished root this chat is + /// centered on (e.g. the agent's process root for this chat, the default + /// location for relative paths). MUST be one of this chat's effective working /// directories ({@link workingDirectories}, or the session's set when that is /// absent). Present when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, - /// the chat inherits {@link SessionState.primaryWorkingDirectory | the - /// session's primary}. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}. + /// + /// **Read-only and fixed at creation.** It is set from + /// {@link CreateChatParams.primaryWorkingDirectory} (or, for the session's + /// default chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does + /// not change over the chat's lifetime — there is no action to mutate it, and + /// it does not participate in `session/chatUpdated`. #[serde(default, skip_serializing_if = "Option::is_none")] pub primary_working_directory: Option, /// Completed turns @@ -1146,22 +1151,14 @@ pub struct SessionState { pub project: Option, /// The working directories the session's agent has tool access to, as /// maintained by the `session/workingDirectorySet` / - /// `session/workingDirectoryRemoved` actions. Directories are equal peers - /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - /// one of them is designated the primary (see {@link primaryWorkingDirectory}). - /// Individual chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - /// chat that sets none operates against this full set. + /// `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + /// the session has no primary. Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + /// designate one of their own directories as primary (see + /// {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + /// operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. Present - /// when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - /// the agent has no primary (all directories equal peers). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub primary_working_directory: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1441,22 +1438,14 @@ pub struct SessionSummary { pub project: Option, /// The working directories the session's agent has tool access to, as /// maintained by the `session/workingDirectorySet` / - /// `session/workingDirectoryRemoved` actions. Directories are equal peers - /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - /// one of them is designated the primary (see {@link primaryWorkingDirectory}). - /// Individual chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - /// chat that sets none operates against this full set. + /// `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + /// the session has no primary. Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + /// designate one of their own directories as primary (see + /// {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + /// operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. Present - /// when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - /// the agent has no primary (all directories equal peers). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub primary_working_directory: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 5392beead..4dfb4f985 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -670,9 +670,6 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - if let Some(working_directories) = &a.changes.working_directories { chat.working_directories = Some(working_directories.clone()); } - if let Some(primary_working_directory) = &a.changes.primary_working_directory { - chat.primary_working_directory = Some(primary_working_directory.clone()); - } ReduceOutcome::Applied } StateAction::SessionDefaultChatChanged(a) => { @@ -1952,7 +1949,6 @@ mod tests { activity: None, project: None, working_directories: None, - primary_working_directory: None, annotations: None, lifecycle: SessionLifecycle::Creating, creation_error: None, diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs index 27784fed9..9031a7658 100644 --- a/clients/rust/crates/ahp/tests/hosts.rs +++ b/clients/rust/crates/ahp/tests/hosts.rs @@ -1040,7 +1040,6 @@ fn make_summary(uri: &str, title: &str, modified_at: i64) -> ahp_types::state::S modified_at: modified, project: None, working_directories: None, - primary_working_directory: None, changes: None, annotations: None, meta: None, diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index ac0790d85..23969f940 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -52,7 +52,6 @@ fn session_state(title: &str, _resource: &str) -> SessionState { activity: None, project: None, working_directories: None, - primary_working_directory: None, annotations: None, lifecycle: SessionLifecycle::Ready, creation_error: None, @@ -356,7 +355,6 @@ fn non_action_event_is_ignored() { modified_at: "1970-01-01T00:00:00.000Z".into(), project: None, working_directories: None, - primary_working_directory: None, changes: None, annotations: None, meta: None, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 7539388a0..6e1e8fd11 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -371,13 +371,15 @@ public struct CreateSessionParams: Codable, Sendable { /// Ignored for forked sessions — a fork inherits its working directories /// from the source session identified by `fork`. public var workingDirectories: [String]? - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. A client - /// SHOULD supply this when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY - /// reject creation that omits it, or fall back to the first entry of + /// The primary working directory for the session's **default chat** — the + /// distinguished root that chat is centered on (see + /// {@link ChatState.primaryWorkingDirectory}). A session has no primary of its + /// own; this seeds the default chat's primary. When set, it MUST be one of + /// {@link workingDirectories}. A client SHOULD supply this when the agent + /// advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a + /// host MAY reject creation that omits it, or fall back to the first entry of /// `workingDirectories`. Ignored for forked sessions (a fork inherits the - /// source session's primary). + /// source session's chats and their primaries). public var primaryWorkingDirectory: String? /// Fork from an existing session. The new session is populated with content /// from the source session up to and including the specified turn's response. @@ -469,13 +471,15 @@ public struct CreateChatParams: Codable, Sendable { /// A client MUST NOT supply this field unless the agent advertises /// {@link AgentCapabilities.multipleWorkingDirectories}. public var workingDirectories: [String]? - /// The chat's primary working directory — the distinguished root this chat - /// centers on. When set, it MUST be one of the chat's effective working + /// The chat's primary working directory — the distinguished root this chat is + /// centered on. When set, it MUST be one of the chat's effective working /// directories ({@link workingDirectories}, or the session's set when that is /// omitted). A client SHOULD supply this when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat - /// narrows to a subset that excludes the session's primary; when absent, the - /// chat inherits the session's primary. Ignored for forked chats. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + /// reject creation that omits it, or fall back to the first of the chat's + /// directories. Fixed at creation and reported (read-only) on + /// {@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork + /// inherits the source chat's primary). public var primaryWorkingDirectory: String? public init( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 086a8677a..7c24cd6a3 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -184,20 +184,13 @@ public struct PartialSessionSummary: Codable, Sendable { public var project: ProjectInfo? /// The working directories the session's agent has tool access to, as /// maintained by the `session/workingDirectorySet` / - /// `session/workingDirectoryRemoved` actions. Directories are equal peers - /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - /// one of them is designated the primary (see {@link primaryWorkingDirectory}). - /// Individual chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - /// chat that sets none operates against this full set. + /// `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + /// the session has no primary. Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + /// designate one of their own directories as primary (see + /// {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + /// operates against this full set. public var workingDirectories: [String]? - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. Present - /// when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - /// the agent has no primary (all directories equal peers). - public var primaryWorkingDirectory: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -227,7 +220,6 @@ public struct PartialSessionSummary: Codable, Sendable { case activity case project case workingDirectories - case primaryWorkingDirectory case annotations case resource case createdAt @@ -243,7 +235,6 @@ public struct PartialSessionSummary: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, - primaryWorkingDirectory: String? = nil, annotations: AnnotationsSummary? = nil, resource: String? = nil, createdAt: String? = nil, @@ -257,7 +248,6 @@ public struct PartialSessionSummary: Codable, Sendable { self.activity = activity self.project = project self.workingDirectories = workingDirectories - self.primaryWorkingDirectory = primaryWorkingDirectory self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index d2ac65037..3c9dacc61 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -665,15 +665,15 @@ public struct MultipleChatsCapability: Codable, Sendable { } public struct MultipleWorkingDirectoriesCapability: Codable, Sendable { - /// The agent requires exactly one of its working directories to be designated - /// the **primary** — a distinguished root the agent centers on (e.g. its - /// process root, the default location for relative paths). When `true`, a - /// client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} - /// (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that - /// narrows the set); a host MAY reject creation that omits it, or fall back to - /// the first entry of `workingDirectories`. The chosen primary is reported on - /// {@link SessionState.primaryWorkingDirectory} / - /// {@link ChatState.primaryWorkingDirectory}. + /// The agent requires each chat to designate one of its working directories as + /// the **primary** — a distinguished root the chat is centered on (e.g. the + /// agent's process root for that chat, the default location for relative + /// paths). Primary is a **per-chat** notion, fixed at chat creation. When + /// `true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory} + /// (and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the + /// session's default chat); a host MAY reject creation that omits it, or fall + /// back to the first entry of the chat's working directories. The chosen + /// primary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}. /// /// When absent or `false`, the agent has no primary — all directories are /// equal peers and clients need not designate one. @@ -914,13 +914,18 @@ public struct ChatState: Codable, Sendable { /// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to /// update the subset on a running chat. public var workingDirectories: [String]? - /// The chat's primary working directory — the distinguished root this chat - /// centers on. When set, it MUST be one of this chat's effective working + /// The chat's primary working directory — the distinguished root this chat is + /// centered on (e.g. the agent's process root for this chat, the default + /// location for relative paths). MUST be one of this chat's effective working /// directories ({@link workingDirectories}, or the session's set when that is /// absent). Present when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, - /// the chat inherits {@link SessionState.primaryWorkingDirectory | the - /// session's primary}. + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}. + /// + /// **Read-only and fixed at creation.** It is set from + /// {@link CreateChatParams.primaryWorkingDirectory} (or, for the session's + /// default chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does + /// not change over the chat's lifetime — there is no action to mutate it, and + /// it does not participate in `session/chatUpdated`. public var primaryWorkingDirectory: String? /// Completed turns public var turns: [Turn] @@ -1070,20 +1075,13 @@ public struct SessionState: Codable, Sendable { public var project: ProjectInfo? /// The working directories the session's agent has tool access to, as /// maintained by the `session/workingDirectorySet` / - /// `session/workingDirectoryRemoved` actions. Directories are equal peers - /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - /// one of them is designated the primary (see {@link primaryWorkingDirectory}). - /// Individual chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - /// chat that sets none operates against this full set. + /// `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + /// the session has no primary. Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + /// designate one of their own directories as primary (see + /// {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + /// operates against this full set. public var workingDirectories: [String]? - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. Present - /// when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - /// the agent has no primary (all directories equal peers). - public var primaryWorkingDirectory: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1169,7 +1167,6 @@ public struct SessionState: Codable, Sendable { case activity case project case workingDirectories - case primaryWorkingDirectory case annotations case lifecycle case creationError @@ -1191,7 +1188,6 @@ public struct SessionState: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, - primaryWorkingDirectory: String? = nil, annotations: AnnotationsSummary? = nil, lifecycle: SessionLifecycle, creationError: ErrorInfo? = nil, @@ -1211,7 +1207,6 @@ public struct SessionState: Codable, Sendable { self.activity = activity self.project = project self.workingDirectories = workingDirectories - self.primaryWorkingDirectory = primaryWorkingDirectory self.annotations = annotations self.lifecycle = lifecycle self.creationError = creationError @@ -1399,20 +1394,13 @@ public struct SessionSummary: Codable, Sendable { public var project: ProjectInfo? /// The working directories the session's agent has tool access to, as /// maintained by the `session/workingDirectorySet` / - /// `session/workingDirectoryRemoved` actions. Directories are equal peers - /// except when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - /// one of them is designated the primary (see {@link primaryWorkingDirectory}). - /// Individual chats MAY restrict to a subset via - /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - /// chat that sets none operates against this full set. + /// `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + /// the session has no primary. Individual chats MAY restrict to a subset via + /// {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + /// designate one of their own directories as primary (see + /// {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + /// operates against this full set. public var workingDirectories: [String]? - /// The session's primary working directory — the distinguished root the agent - /// centers on. When set, it MUST be one of {@link workingDirectories}. Present - /// when the agent advertises - /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - /// the agent has no primary (all directories equal peers). - public var primaryWorkingDirectory: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1442,7 +1430,6 @@ public struct SessionSummary: Codable, Sendable { case activity case project case workingDirectories - case primaryWorkingDirectory case annotations case resource case createdAt @@ -1458,7 +1445,6 @@ public struct SessionSummary: Codable, Sendable { activity: String? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, - primaryWorkingDirectory: String? = nil, annotations: AnnotationsSummary? = nil, resource: String, createdAt: String, @@ -1472,7 +1458,6 @@ public struct SessionSummary: Codable, Sendable { self.activity = activity self.project = project self.workingDirectories = workingDirectories - self.primaryWorkingDirectory = primaryWorkingDirectory self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 025e801bc..dc7027fcd 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -930,7 +930,6 @@ private func mergeChatSummaryChanges(_ summary: inout ChatSummary, changes: Part if let modifiedAt = changes.modifiedAt { summary.modifiedAt = modifiedAt } if let origin = changes.origin { summary.origin = origin } if let workingDirectories = changes.workingDirectories { summary.workingDirectories = workingDirectories } - if let primaryWorkingDirectory = changes.primaryWorkingDirectory { summary.primaryWorkingDirectory = primaryWorkingDirectory } } private func chatSummaryStatus(_ state: ChatState, terminalStatus: SessionStatus? = nil) -> SessionStatus { diff --git a/docs/.changes/20260716-multiroot-sessions.json b/docs/.changes/20260716-multiroot-sessions.json index 42b32024f..b52dbaac9 100644 --- a/docs/.changes/20260716-multiroot-sessions.json +++ b/docs/.changes/20260716-multiroot-sessions.json @@ -1,5 +1,5 @@ { "type": "added", - "message": "Multiroot session support: `AgentCapabilities.multipleWorkingDirectories` capability (with `requiresPrimary`), `CreateSessionParams.workingDirectories` / `primaryWorkingDirectory` and `SessionMetadata.workingDirectories` / `primaryWorkingDirectory` (equal-peer set plus an optional designated primary, mirrored onto `SessionState`/`SessionSummary`), `CreateChatParams.workingDirectories` / `primaryWorkingDirectory` and `ChatState`/`ChatSummary.workingDirectories` / `primaryWorkingDirectory` (a subset of the session's set with its own optional primary), and the client-dispatchable `session/workingDirectorySet` / `session/workingDirectoryRemoved` and `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions for mutating those sets.", + "message": "Multiroot session support: `AgentCapabilities.multipleWorkingDirectories` capability (with `requiresPrimary`), `CreateSessionParams.workingDirectories` and `SessionMetadata.workingDirectories` (an equal-peer set, mirrored onto `SessionState`/`SessionSummary`), `CreateChatParams.workingDirectories` and `ChatState`/`ChatSummary.workingDirectories` (a subset of the session's set), a per-chat read-only `ChatState`/`ChatSummary.primaryWorkingDirectory` (fixed at chat creation; set via `CreateChatParams.primaryWorkingDirectory`, or `CreateSessionParams.primaryWorkingDirectory` for the default chat), and the client-dispatchable `session/workingDirectorySet` / `session/workingDirectoryRemoved` and `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` actions for mutating those sets.", "issues": [337] } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 05e0ca8bb..c99532069 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -74,7 +74,6 @@ SessionState { activity?: string project?: ProjectInfo workingDirectories?: URI[] // equal-peer working directories - primaryWorkingDirectory?: URI // designated primary (when requiresPrimary) annotations?: AnnotationsSummary lifecycle: 'creating' | 'ready' | 'creationFailed' @@ -109,7 +108,6 @@ SessionSummary { modifiedAt: string // ISO 8601 project?: ProjectInfo workingDirectories?: URI[] // equal-peer working directories - primaryWorkingDirectory?: URI // designated primary (when requiresPrimary) annotations?: AnnotationsSummary changes?: ChangesSummary } @@ -153,7 +151,7 @@ ChatState { modifiedAt: string origin?: ChatOrigin // how the chat came to exist (user / fork / tool) workingDirectories?: URI[] // subset of session's workingDirectories - primaryWorkingDirectory?: URI // this chat's primary (inherits session's when absent) + primaryWorkingDirectory?: URI // this chat's primary, read-only (set at creation, when requiresPrimary) turns: Turn[] // completed turns turnsNextCursor?: string // page older turns via fetchTurns @@ -568,14 +566,16 @@ The forked session is an independent copy — subsequent changes to either sessi ## Multiroot Sessions A session can be granted tool access to more than one working directory when the -agent advertises the `multipleWorkingDirectories` capability. All directories are -**equal peers** — there is no privileged "primary" — unless the agent advertises -`multipleWorkingDirectories.requiresPrimary`, in which case one directory is -designated the **primary** (see below). +agent advertises the `multipleWorkingDirectories` capability. At the session +level the directories are always **equal peers** — a session has no primary. A +**primary** is a per-chat notion (see [below](#per-chat-working-directory-subsets)); +the session simply owns the set every chat draws from. ### Creating a multiroot session -Pass `workingDirectories` (plural) in `createSession`: +Pass `workingDirectories` (plural) in `createSession`. When the agent +`requiresPrimary`, also pass `primaryWorkingDirectory` — it seeds the primary of +the session's **default chat** (the session itself stores no primary): ```typescript createSession({ @@ -585,7 +585,7 @@ createSession({ 'file:///workspace/frontend', 'file:///workspace/backend', ], - primaryWorkingDirectory: 'file:///workspace/frontend', // only when requiresPrimary + primaryWorkingDirectory: 'file:///workspace/frontend', // seeds default chat, when requiresPrimary }); ``` @@ -595,14 +595,12 @@ first entry as the session's working directory and ignore the rest. When the agent advertises `multipleWorkingDirectories.requiresPrimary`, a client SHOULD supply `primaryWorkingDirectory` (which MUST be one of -`workingDirectories`) to designate the distinguished root the agent centers on. -A host MAY reject a creation that omits it, or fall back to the first entry. The -chosen primary is reported on `SessionState.primaryWorkingDirectory`. When the -agent has no primary, all directories are equal peers and no designation is -needed. +`workingDirectories`); a host MAY reject a creation that omits it, or fall back +to the first entry. It becomes the default chat's read-only +`ChatState.primaryWorkingDirectory`. Forked sessions ignore `workingDirectories` / `primaryWorkingDirectory` — they -inherit the working directories (and primary) of the source session. +inherit the working directories (and per-chat primaries) of the source session. ### Managing directories after creation @@ -612,7 +610,7 @@ mutate it by **dispatching actions**, not by calling commands: | Action | Effect | | --- | --- | | `session/workingDirectorySet` | Adds `directory` to the set (creating it if absent). A no-op when the directory is already present. | -| `session/workingDirectoryRemoved` | Removes `directory` from the set. A no-op when it is not present. There is no atomic backend "remove one" primitive — the host reconfigures its agent to the reduced set. A host MAY decline to apply the removal (e.g. the current primary of an agent that requires one), leaving the set unchanged. | +| `session/workingDirectoryRemoved` | Removes `directory` from the set. A no-op when it is not present. There is no atomic backend "remove one" primitive — the host reconfigures its agent to the reduced set. A host MAY decline to apply the removal (e.g. a directory still designated as some chat's primary), leaving the set unchanged. | Both are `@clientDispatchable`. The resulting set is observed on `SessionState.workingDirectories` like any other state — there is no separate @@ -632,23 +630,30 @@ The effective set for a chat is recorded in `ChatState.workingDirectories` / `ChatSummary.workingDirectories`. When absent the chat inherits the full session set. +A chat also designates its own **primary** working directory — +`ChatState.primaryWorkingDirectory` (mirrored on `ChatSummary`). It is +**read-only and fixed at chat creation**: there is no action to change it, and +it does not participate in `session/chatUpdated`. Present only when the agent +`requiresPrimary`. Each chat can pick a different primary from its own effective +directories. + #### Setting at create time Pass `workingDirectories` to `createChat`. Every entry must already exist in the session's `workingDirectories`. When the agent `requiresPrimary`, also pass -`primaryWorkingDirectory` if the chat's primary differs from the session's: +`primaryWorkingDirectory` (one of the chat's effective directories): ```typescript createChat({ channel: 'ahp-session:/', chat: 'ahp-chat:/', workingDirectories: ['file:///workspace/frontend'], // subset - primaryWorkingDirectory: 'file:///workspace/frontend', // optional; inherits session's when omitted + primaryWorkingDirectory: 'file:///workspace/frontend', // this chat's primary, when requiresPrimary }); ``` Forked chats (those with a `source`) inherit the source chat's -`workingDirectories`; the field is ignored for them. +`workingDirectories` and primary; both fields are ignored for them. #### Managing the subset after creation diff --git a/docs/proposals/multiroot-sessions.md b/docs/proposals/multiroot-sessions.md index 8d977fe22..0887b60d6 100644 --- a/docs/proposals/multiroot-sessions.md +++ b/docs/proposals/multiroot-sessions.md @@ -169,12 +169,13 @@ throughout — no juggling three disconnected sessions. - **It is not per-file or per-glob scoping.** The unit is a working directory (a root), not arbitrary path patterns within it. -- **No mandatory primary.** All directories are peers by default. A backend that - *needs* one distinguished root advertises `requiresPrimary` on the capability; - the client then designates it explicitly via `primaryWorkingDirectory` (on - `createSession`/`createChat`, reported back in state). The protocol has no - other notion of a "main" directory, and — deliberately — primacy is **not** - tied to array position. +- **No mandatory primary; when needed, it's per-chat.** All directories are + peers by default and the **session has no primary at all**. A backend that + *needs* one distinguished root advertises `requiresPrimary`; the client then + designates it explicitly per **chat** via `primaryWorkingDirectory` (on + `createChat`, or `createSession` to seed the default chat). It is read-only + and fixed at chat creation — never inferred from array position, never mutated + after birth. - **It is not multi-root *chats as sub-sessions*.** A chat narrowing to a subset is still one thread under one session's trust and identity. Independent agents @@ -234,7 +235,7 @@ as today. | `AgentCapabilities.multipleWorkingDirectories?` | `channels-root/state.ts` | added (capability) | | `MultipleWorkingDirectoriesCapability` (`requiresPrimary?`) | `channels-root/state.ts` | added (type) | | `CreateSessionParams.workingDirectories?` / `primaryWorkingDirectory?` | `channels-session/commands.ts` | added | -| `SessionMetadata.workingDirectories?` / `primaryWorkingDirectory?` (→ `SessionState`, `SessionSummary`) | `channels-session/state.ts` | added | +| `SessionMetadata.workingDirectories?` (→ `SessionState`, `SessionSummary`) | `channels-session/state.ts` | added | | `session/workingDirectorySet` / `session/workingDirectoryRemoved` actions | `channels-session/actions.ts` | added (action) | | `ChatState.workingDirectories?` / `primaryWorkingDirectory?` (& `ChatSummary`) | `channels-chat/state.ts` | added | | `CreateChatParams.workingDirectories?` / `primaryWorkingDirectory?` | `channels-chat/commands.ts` | added | @@ -273,15 +274,14 @@ interface CreateSessionParams extends BaseParams { // …existing… /** The session's equal-peer working directories. */ workingDirectories?: URI[]; - /** Designated primary (⊆ workingDirectories); supply when requiresPrimary. */ + /** Seeds the default chat's primary (⊆ workingDirectories); when requiresPrimary. */ primaryWorkingDirectory?: URI; } // ── Session state — channels-session/state.ts (→ SessionState/SessionSummary) interface SessionMetadata { // …existing… - workingDirectories?: URI[]; - primaryWorkingDirectory?: URI; // host's authoritative "which is primary" + workingDirectories?: URI[]; // equal peers; the session has NO primary } // ── Session runtime mutation — channels-session/actions.ts ──────────────── @@ -300,12 +300,13 @@ interface ChatState /* and ChatSummary */ { // …existing… /** The chat's subset — every entry MUST be one of the session's dirs. */ workingDirectories?: URI[]; - primaryWorkingDirectory?: URI; // chat's primary; inherits session's when absent + /** Read-only, fixed at creation; no action, not in chatUpdated. */ + primaryWorkingDirectory?: URI; } interface CreateChatParams extends BaseParams { // …existing… workingDirectories?: URI[]; // subset ⊆ session; absent → whole session set - primaryWorkingDirectory?: URI; // ⊆ chat's dirs; inherits session's when absent + primaryWorkingDirectory?: URI; // ⊆ chat's dirs; when requiresPrimary } // channel = chat URI. Gated by multipleWorkingDirectories. @clientDispatchable. interface ChatWorkingDirectorySetAction { @@ -337,22 +338,27 @@ interface ChatWorkingDirectoryRemovedAction { handshake. - Removal actions are idempotent and modelled as *reconfigure-to-the-reduced-set*; a host MAY decline to apply a removal (e.g. - the current primary directory of an agent that `requiresPrimary`), leaving the - set unchanged. + a directory still designated as some chat's primary), leaving the set + unchanged. --- ## 9. Design decisions & resolved questions -- **No mandatory primary; when needed, it's explicit.** *Resolved:* the set is - equal peers by default. Rejected a "primary + additional" split because it - re-introduces the confusion the user called out ("what's the relation between - primary and secondary?"). An agent that genuinely needs one distinguished root - advertises `MultipleWorkingDirectoriesCapability.requiresPrimary`; the client - then designates it **explicitly** via `primaryWorkingDirectory` on - `createSession` / `createChat` (reported back in state). Deliberately *not* - positional — "primary" is never inferred from array index, so removals and - reordering stay safe. +- **Primary is per-chat, read-only, fixed at creation.** *Resolved:* the set is + equal peers by default and the **session has no primary**. Rejected a + "primary + additional" split because it re-introduces the confusion the user + called out ("what's the relation between primary and secondary?"). An agent + that needs one distinguished root advertises `requiresPrimary`; the client + designates it **per chat** via `primaryWorkingDirectory` + (`createChat`, or `createSession` to seed the default chat). It is stored + read-only on `ChatState`/`ChatSummary` (user-visible, recoverable by late + subscribers) but has **no mutating action** and is not part of + `session/chatUpdated` — so it's visible yet immutable. "In state" and + "mutable" are orthogonal in AHP: mutability comes from an action existing, not + from where the value lives. Deliberately *not* positional — never inferred + from array index — and a role-on-entry shape was rejected because it would + couple the immutable primary to the mutable set's lifecycle. - **Hard-remove the singular `workingDirectory`.** *Resolved (per review):* removing these fields is a breaking change, so the feature targets a MINOR @@ -387,8 +393,8 @@ interface ChatWorkingDirectoryRemovedAction { - **Removal semantics.** *Resolved:* no single-remove primitive is assumed; the `*/workingDirectoryRemoved` action reduces to the reduced set, so it is - idempotent and safe to retry. A host MAY decline (e.g. the current primary of - an agent that `requiresPrimary`). + idempotent and safe to retry. A host MAY decline (e.g. a directory still + designated as some chat's primary). --- diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 2a9f0ccdf..eebb5e579 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -431,7 +431,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. the current primary directory of an agent\nthat {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a\nprimary}); it then leaves the set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. a directory still designated as some\nchat's {@link ChatState.primaryWorkingDirectory | primary}); it then leaves\nthe set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" @@ -2741,7 +2741,7 @@ "properties": { "requiresPrimary": { "type": "boolean", - "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." + "description": "The agent requires each chat to designate one of its working directories as\nthe **primary** — a distinguished root the chat is centered on (e.g. the\nagent's process root for that chat, the default location for relative\npaths). Primary is a **per-chat** notion, fixed at chat creation. When\n`true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory}\n(and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the\nsession's default chat); a host MAY reject creation that omits it, or fall\nback to the first entry of the chat's working directories. The chosen\nprimary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -2864,11 +2864,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2910,11 +2906,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3218,11 +3210,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -4511,7 +4499,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + "description": "The chat's primary working directory — the distinguished root this chat is\ncentered on (e.g. the agent's process root for this chat, the default\nlocation for relative paths). MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}.\n\n**Read-only and fixed at creation.** It is set from\n{@link CreateChatParams.primaryWorkingDirectory} (or, for the session's\ndefault chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does\nnot change over the chat's lifetime — there is no action to mutate it, and\nit does not participate in `session/chatUpdated`." }, "turns": { "type": "array", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 2619dc00d..ffd7153a1 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -971,7 +971,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. A client\nSHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's primary)." + "description": "The primary working directory for the session's **default chat** — the\ndistinguished root that chat is centered on (see\n{@link ChatState.primaryWorkingDirectory}). A session has no primary of its\nown; this seeds the default chat's primary. When set, it MUST be one of\n{@link workingDirectories}. A client SHOULD supply this when the agent\nadvertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a\nhost MAY reject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's chats and their primaries)." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -1147,7 +1147,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of the chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nomitted). A client SHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat\nnarrows to a subset that excludes the session's primary; when absent, the\nchat inherits the session's primary. Ignored for forked chats." + "description": "The chat's primary working directory — the distinguished root this chat is\ncentered on. When set, it MUST be one of the chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nomitted). A client SHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first of the chat's\ndirectories. Fixed at creation and reported (read-only) on\n{@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork\ninherits the source chat's primary)." } }, "required": [ @@ -1884,7 +1884,7 @@ "properties": { "requiresPrimary": { "type": "boolean", - "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." + "description": "The agent requires each chat to designate one of its working directories as\nthe **primary** — a distinguished root the chat is centered on (e.g. the\nagent's process root for that chat, the default location for relative\npaths). Primary is a **per-chat** notion, fixed at chat creation. When\n`true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory}\n(and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the\nsession's default chat); a host MAY reject creation that omits it, or fall\nback to the first entry of the chat's working directories. The chosen\nprimary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -2007,11 +2007,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2053,11 +2049,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2361,11 +2353,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3654,7 +3642,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + "description": "The chat's primary working directory — the distinguished root this chat is\ncentered on (e.g. the agent's process root for this chat, the default\nlocation for relative paths). MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}.\n\n**Read-only and fixed at creation.** It is set from\n{@link CreateChatParams.primaryWorkingDirectory} (or, for the session's\ndefault chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does\nnot change over the chat's lifetime — there is no action to mutate it, and\nit does not participate in `session/chatUpdated`." }, "turns": { "type": "array", @@ -6588,7 +6576,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. the current primary directory of an agent\nthat {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a\nprimary}); it then leaves the set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. a directory still designated as some\nchat's {@link ChatState.primaryWorkingDirectory | primary}); it then leaves\nthe set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" diff --git a/schema/errors.schema.json b/schema/errors.schema.json index c725289df..f20f5d93b 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -650,7 +650,7 @@ "properties": { "requiresPrimary": { "type": "boolean", - "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." + "description": "The agent requires each chat to designate one of its working directories as\nthe **primary** — a distinguished root the chat is centered on (e.g. the\nagent's process root for that chat, the default location for relative\npaths). Primary is a **per-chat** notion, fixed at chat creation. When\n`true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory}\n(and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the\nsession's default chat); a host MAY reject creation that omits it, or fall\nback to the first entry of the chat's working directories. The chosen\nprimary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -773,11 +773,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -819,11 +815,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1127,11 +1119,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2420,7 +2408,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + "description": "The chat's primary working directory — the distinguished root this chat is\ncentered on (e.g. the agent's process root for this chat, the default\nlocation for relative paths). MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}.\n\n**Read-only and fixed at creation.** It is set from\n{@link CreateChatParams.primaryWorkingDirectory} (or, for the session's\ndefault chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does\nnot change over the chat's lifetime — there is no action to mutate it, and\nit does not participate in `session/chatUpdated`." }, "turns": { "type": "array", @@ -5894,7 +5882,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. A client\nSHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's primary)." + "description": "The primary working directory for the session's **default chat** — the\ndistinguished root that chat is centered on (see\n{@link ChatState.primaryWorkingDirectory}). A session has no primary of its\nown; this seeds the default chat's primary. When set, it MUST be one of\n{@link workingDirectories}. A client SHOULD supply this when the agent\nadvertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a\nhost MAY reject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's chats and their primaries)." }, "fork": { "$ref": "#/$defs/SessionForkSource", @@ -6070,7 +6058,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of the chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nomitted). A client SHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat\nnarrows to a subset that excludes the session's primary; when absent, the\nchat inherits the session's primary. Ignored for forked chats." + "description": "The chat's primary working directory — the distinguished root this chat is\ncentered on. When set, it MUST be one of the chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nomitted). A client SHOULD supply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first of the chat's\ndirectories. Fixed at creation and reported (read-only) on\n{@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork\ninherits the source chat's primary)." } }, "required": [ @@ -7492,7 +7480,7 @@ }, "SessionWorkingDirectoryRemovedAction": { "type": "object", - "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. the current primary directory of an agent\nthat {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a\nprimary}); it then leaves the set unchanged.", + "description": "A working directory was removed from the session's\n{@link SessionState.workingDirectories} set.\n\nRemoves `directory` from the set; a no-op when it is not present. There is no\natomic backend \"remove one\" primitive — a host reconfigures its agent to the\nreduced set — so this action is safe to model as idempotent. A host MAY\ndecline to apply the removal (e.g. a directory still designated as some\nchat's {@link ChatState.primaryWorkingDirectory | primary}); it then leaves\nthe set unchanged.", "properties": { "type": { "const": "session/workingDirectoryRemoved" diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 22d92c0ad..2319bd5c5 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -103,11 +103,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -817,7 +813,7 @@ "properties": { "requiresPrimary": { "type": "boolean", - "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." + "description": "The agent requires each chat to designate one of its working directories as\nthe **primary** — a distinguished root the chat is centered on (e.g. the\nagent's process root for that chat, the default location for relative\npaths). Primary is a **per-chat** notion, fixed at chat creation. When\n`true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory}\n(and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the\nsession's default chat); a host MAY reject creation that omits it, or fall\nback to the first entry of the chat's working directories. The chosen\nprimary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -940,11 +936,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -986,11 +978,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1294,11 +1282,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2587,7 +2571,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + "description": "The chat's primary working directory — the distinguished root this chat is\ncentered on (e.g. the agent's process root for this chat, the default\nlocation for relative paths). MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}.\n\n**Read-only and fixed at creation.** It is set from\n{@link CreateChatParams.primaryWorkingDirectory} (or, for the session's\ndefault chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does\nnot change over the chat's lifetime — there is no action to mutate it, and\nit does not participate in `session/chatUpdated`." }, "turns": { "type": "array", diff --git a/schema/state.schema.json b/schema/state.schema.json index 25bacc77d..f82a33c76 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -561,7 +561,7 @@ "properties": { "requiresPrimary": { "type": "boolean", - "description": "The agent requires exactly one of its working directories to be designated\nthe **primary** — a distinguished root the agent centers on (e.g. its\nprocess root, the default location for relative paths). When `true`, a\nclient SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory}\n(and {@link CreateChatParams.primaryWorkingDirectory} for a chat that\nnarrows the set); a host MAY reject creation that omits it, or fall back to\nthe first entry of `workingDirectories`. The chosen primary is reported on\n{@link SessionState.primaryWorkingDirectory} /\n{@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." + "description": "The agent requires each chat to designate one of its working directories as\nthe **primary** — a distinguished root the chat is centered on (e.g. the\nagent's process root for that chat, the default location for relative\npaths). Primary is a **per-chat** notion, fixed at chat creation. When\n`true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory}\n(and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the\nsession's default chat); a host MAY reject creation that omits it, or fall\nback to the first entry of the chat's working directories. The chosen\nprimary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}.\n\nWhen absent or `false`, the agent has no primary — all directories are\nequal peers and clients need not designate one." } } }, @@ -684,11 +684,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -730,11 +726,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1038,11 +1030,7 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case\none of them is designated the primary (see {@link primaryWorkingDirectory}).\nIndividual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." - }, - "primaryWorkingDirectory": { - "$ref": "#/$defs/URI", - "description": "The session's primary working directory — the distinguished root the agent\ncenters on. When set, it MUST be one of {@link workingDirectories}. Present\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when\nthe agent has no primary (all directories equal peers)." + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are **equal peers** —\nthe session has no primary. Individual chats MAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`} and\ndesignate one of their own directories as primary (see\n{@link ChatState.primaryWorkingDirectory}); a chat that sets no subset\noperates against this full set." }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2331,7 +2319,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The chat's primary working directory — the distinguished root this chat\ncenters on. When set, it MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent,\nthe chat inherits {@link SessionState.primaryWorkingDirectory | the\nsession's primary}." + "description": "The chat's primary working directory — the distinguished root this chat is\ncentered on (e.g. the agent's process root for this chat, the default\nlocation for relative paths). MUST be one of this chat's effective working\ndirectories ({@link workingDirectories}, or the session's set when that is\nabsent). Present when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}.\n\n**Read-only and fixed at creation.** It is set from\n{@link CreateChatParams.primaryWorkingDirectory} (or, for the session's\ndefault chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does\nnot change over the chat's lifetime — there is no action to mutate it, and\nit does not participate in `session/chatUpdated`." }, "turns": { "type": "array", diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index cf5f61762..0e78d31c6 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -50,13 +50,15 @@ export interface CreateChatParams extends BaseParams { */ workingDirectories?: URI[]; /** - * The chat's primary working directory — the distinguished root this chat - * centers on. When set, it MUST be one of the chat's effective working + * The chat's primary working directory — the distinguished root this chat is + * centered on. When set, it MUST be one of the chat's effective working * directories ({@link workingDirectories}, or the session's set when that is * omitted). A client SHOULD supply this when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary} and the chat - * narrows to a subset that excludes the session's primary; when absent, the - * chat inherits the session's primary. Ignored for forked chats. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + * reject creation that omits it, or fall back to the first of the chat's + * directories. Fixed at creation and reported (read-only) on + * {@link ChatState.primaryWorkingDirectory}. Ignored for forked chats (a fork + * inherits the source chat's primary). */ primaryWorkingDirectory?: URI; } diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index b423f842e..bfccceed5 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -72,13 +72,18 @@ export interface ChatState { */ workingDirectories?: URI[]; /** - * The chat's primary working directory — the distinguished root this chat - * centers on. When set, it MUST be one of this chat's effective working + * The chat's primary working directory — the distinguished root this chat is + * centered on (e.g. the agent's process root for this chat, the default + * location for relative paths). MUST be one of this chat's effective working * directories ({@link workingDirectories}, or the session's set when that is * absent). Present when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; when absent, - * the chat inherits {@link SessionState.primaryWorkingDirectory | the - * session's primary}. + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}. + * + * **Read-only and fixed at creation.** It is set from + * {@link CreateChatParams.primaryWorkingDirectory} (or, for the session's + * default chat, {@link CreateSessionParams.primaryWorkingDirectory}) and does + * not change over the chat's lifetime — there is no action to mutate it, and + * it does not participate in `session/chatUpdated`. */ primaryWorkingDirectory?: URI; diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index 3cd254396..92b4fa08e 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -144,15 +144,15 @@ export interface MultipleChatsCapability { */ export interface MultipleWorkingDirectoriesCapability { /** - * The agent requires exactly one of its working directories to be designated - * the **primary** — a distinguished root the agent centers on (e.g. its - * process root, the default location for relative paths). When `true`, a - * client SHOULD supply {@link CreateSessionParams.primaryWorkingDirectory} - * (and {@link CreateChatParams.primaryWorkingDirectory} for a chat that - * narrows the set); a host MAY reject creation that omits it, or fall back to - * the first entry of `workingDirectories`. The chosen primary is reported on - * {@link SessionState.primaryWorkingDirectory} / - * {@link ChatState.primaryWorkingDirectory}. + * The agent requires each chat to designate one of its working directories as + * the **primary** — a distinguished root the chat is centered on (e.g. the + * agent's process root for that chat, the default location for relative + * paths). Primary is a **per-chat** notion, fixed at chat creation. When + * `true`, a client SHOULD supply {@link CreateChatParams.primaryWorkingDirectory} + * (and {@link CreateSessionParams.primaryWorkingDirectory}, which seeds the + * session's default chat); a host MAY reject creation that omits it, or fall + * back to the first entry of the chat's working directories. The chosen + * primary is reported (read-only) on {@link ChatState.primaryWorkingDirectory}. * * When absent or `false`, the agent has no primary — all directories are * equal peers and clients need not designate one. diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index ccb1377e7..14dbf468c 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -283,9 +283,9 @@ export interface SessionWorkingDirectorySetAction { * Removes `directory` from the set; a no-op when it is not present. There is no * atomic backend "remove one" primitive — a host reconfigures its agent to the * reduced set — so this action is safe to model as idempotent. A host MAY - * decline to apply the removal (e.g. the current primary directory of an agent - * that {@link MultipleWorkingDirectoriesCapability.requiresPrimary | requires a - * primary}); it then leaves the set unchanged. + * decline to apply the removal (e.g. a directory still designated as some + * chat's {@link ChatState.primaryWorkingDirectory | primary}); it then leaves + * the set unchanged. * * @category Session Actions * @version 1 diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index fa63165da..fd5b5368b 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -86,13 +86,15 @@ export interface CreateSessionParams extends BaseParams { */ workingDirectories?: URI[]; /** - * The session's primary working directory — the distinguished root the agent - * centers on. When set, it MUST be one of {@link workingDirectories}. A client - * SHOULD supply this when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY - * reject creation that omits it, or fall back to the first entry of + * The primary working directory for the session's **default chat** — the + * distinguished root that chat is centered on (see + * {@link ChatState.primaryWorkingDirectory}). A session has no primary of its + * own; this seeds the default chat's primary. When set, it MUST be one of + * {@link workingDirectories}. A client SHOULD supply this when the agent + * advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a + * host MAY reject creation that omits it, or fall back to the first entry of * `workingDirectories`. Ignored for forked sessions (a fork inherits the - * source session's primary). + * source session's chats and their primaries). */ primaryWorkingDirectory?: URI; /** diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 32b44e809..3d8739290 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -86,23 +86,14 @@ export interface SessionMetadata { /** * The working directories the session's agent has tool access to, as * maintained by the `session/workingDirectorySet` / - * `session/workingDirectoryRemoved` actions. Directories are equal peers - * except when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}, in which case - * one of them is designated the primary (see {@link primaryWorkingDirectory}). - * Individual chats MAY restrict to a subset via - * {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a - * chat that sets none operates against this full set. + * `session/workingDirectoryRemoved` actions. Directories are **equal peers** — + * the session has no primary. Individual chats MAY restrict to a subset via + * {@link ChatSummary.workingDirectories | their own `workingDirectories`} and + * designate one of their own directories as primary (see + * {@link ChatState.primaryWorkingDirectory}); a chat that sets no subset + * operates against this full set. */ workingDirectories?: URI[]; - /** - * The session's primary working directory — the distinguished root the agent - * centers on. When set, it MUST be one of {@link workingDirectories}. Present - * when the agent advertises - * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; absent when - * the agent has no primary (all directories equal peers). - */ - primaryWorkingDirectory?: URI; /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render From d533dbd943aec2ef267f5495a8e602f587d1da57 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 22 Jul 2026 07:02:07 -0700 Subject: [PATCH 15/15] docs: explain why primaryWorkingDirectory is on CreateSessionParams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session has no primary, yet CreateSessionParams carries primaryWorkingDirectory — document that this is because createSession implicitly creates the default chat (there is no separate createChat call for it), so this field is the only place to designate the default chat's primary at birth. Clarified in both the JSDoc and the state-model guide. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahptypes/commands.generated.go | 22 +++++++++++++------ .../generated/Commands.generated.kt | 22 +++++++++++++------ clients/rust/crates/ahp-types/src/commands.rs | 22 +++++++++++++------ .../Generated/Commands.generated.swift | 22 +++++++++++++------ docs/guide/state-model.md | 7 ++++++ schema/commands.schema.json | 2 +- schema/errors.schema.json | 2 +- types/channels-session/commands.ts | 22 +++++++++++++------ 8 files changed, 84 insertions(+), 37 deletions(-) diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 2f7369085..533f9d885 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -322,13 +322,21 @@ type CreateSessionParams struct { // Ignored for forked sessions — a fork inherits its working directories // from the source session identified by `fork`. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // The primary working directory for the session's **default chat** — the - // distinguished root that chat is centered on (see - // {@link ChatState.primaryWorkingDirectory}). A session has no primary of its - // own; this seeds the default chat's primary. When set, it MUST be one of - // {@link workingDirectories}. A client SHOULD supply this when the agent - // advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a - // host MAY reject creation that omits it, or fall back to the first entry of + // The primary working directory for the session's **default chat**. + // + // A session has no primary of its own — primary is a per-chat notion (see + // {@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly + // creates the session's default chat, and there is no separate `createChat` + // call to carry that chat's create-time fields. This field is therefore the + // only place a client can designate the **default chat's** primary at birth; + // it is copied into that chat's read-only `primaryWorkingDirectory`. For any + // non-default chat, pass {@link CreateChatParams.primaryWorkingDirectory} + // instead. + // + // When set, it MUST be one of {@link workingDirectories}. A client SHOULD + // supply this when the agent advertises + // {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + // reject creation that omits it, or fall back to the first entry of // `workingDirectories`. Ignored for forked sessions (a fork inherits the // source session's chats and their primaries). PrimaryWorkingDirectory *URI `json:"primaryWorkingDirectory,omitempty"` diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 93cf8f73c..96ecb2c4a 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -388,13 +388,21 @@ data class CreateSessionParams( */ val workingDirectories: List? = null, /** - * The primary working directory for the session's **default chat** — the - * distinguished root that chat is centered on (see - * {@link ChatState.primaryWorkingDirectory}). A session has no primary of its - * own; this seeds the default chat's primary. When set, it MUST be one of - * {@link workingDirectories}. A client SHOULD supply this when the agent - * advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a - * host MAY reject creation that omits it, or fall back to the first entry of + * The primary working directory for the session's **default chat**. + * + * A session has no primary of its own — primary is a per-chat notion (see + * {@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly + * creates the session's default chat, and there is no separate `createChat` + * call to carry that chat's create-time fields. This field is therefore the + * only place a client can designate the **default chat's** primary at birth; + * it is copied into that chat's read-only `primaryWorkingDirectory`. For any + * non-default chat, pass {@link CreateChatParams.primaryWorkingDirectory} + * instead. + * + * When set, it MUST be one of {@link workingDirectories}. A client SHOULD + * supply this when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + * reject creation that omits it, or fall back to the first entry of * `workingDirectories`. Ignored for forked sessions (a fork inherits the * source session's chats and their primaries). */ diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 1afab4bde..cc4e7a4c5 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -409,13 +409,21 @@ pub struct CreateSessionParams { /// from the source session identified by `fork`. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// The primary working directory for the session's **default chat** — the - /// distinguished root that chat is centered on (see - /// {@link ChatState.primaryWorkingDirectory}). A session has no primary of its - /// own; this seeds the default chat's primary. When set, it MUST be one of - /// {@link workingDirectories}. A client SHOULD supply this when the agent - /// advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a - /// host MAY reject creation that omits it, or fall back to the first entry of + /// The primary working directory for the session's **default chat**. + /// + /// A session has no primary of its own — primary is a per-chat notion (see + /// {@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly + /// creates the session's default chat, and there is no separate `createChat` + /// call to carry that chat's create-time fields. This field is therefore the + /// only place a client can designate the **default chat's** primary at birth; + /// it is copied into that chat's read-only `primaryWorkingDirectory`. For any + /// non-default chat, pass {@link CreateChatParams.primaryWorkingDirectory} + /// instead. + /// + /// When set, it MUST be one of {@link workingDirectories}. A client SHOULD + /// supply this when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + /// reject creation that omits it, or fall back to the first entry of /// `workingDirectories`. Ignored for forked sessions (a fork inherits the /// source session's chats and their primaries). #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 6e1e8fd11..59f6eb603 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -371,13 +371,21 @@ public struct CreateSessionParams: Codable, Sendable { /// Ignored for forked sessions — a fork inherits its working directories /// from the source session identified by `fork`. public var workingDirectories: [String]? - /// The primary working directory for the session's **default chat** — the - /// distinguished root that chat is centered on (see - /// {@link ChatState.primaryWorkingDirectory}). A session has no primary of its - /// own; this seeds the default chat's primary. When set, it MUST be one of - /// {@link workingDirectories}. A client SHOULD supply this when the agent - /// advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a - /// host MAY reject creation that omits it, or fall back to the first entry of + /// The primary working directory for the session's **default chat**. + /// + /// A session has no primary of its own — primary is a per-chat notion (see + /// {@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly + /// creates the session's default chat, and there is no separate `createChat` + /// call to carry that chat's create-time fields. This field is therefore the + /// only place a client can designate the **default chat's** primary at birth; + /// it is copied into that chat's read-only `primaryWorkingDirectory`. For any + /// non-default chat, pass {@link CreateChatParams.primaryWorkingDirectory} + /// instead. + /// + /// When set, it MUST be one of {@link workingDirectories}. A client SHOULD + /// supply this when the agent advertises + /// {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + /// reject creation that omits it, or fall back to the first entry of /// `workingDirectories`. Ignored for forked sessions (a fork inherits the /// source session's chats and their primaries). public var primaryWorkingDirectory: String? diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index c99532069..8b4753486 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -599,6 +599,13 @@ SHOULD supply `primaryWorkingDirectory` (which MUST be one of to the first entry. It becomes the default chat's read-only `ChatState.primaryWorkingDirectory`. +> **Why is `primaryWorkingDirectory` on `createSession` if the session has no +> primary?** Because `createSession` implicitly creates the session's **default +> chat**, and there is no separate `createChat` call to carry that chat's +> create-time fields. This field is the only place to designate the default +> chat's primary at birth. For any additional chat, pass its primary to +> `createChat` instead. + Forked sessions ignore `workingDirectories` / `primaryWorkingDirectory` — they inherit the working directories (and per-chat primaries) of the source session. diff --git a/schema/commands.schema.json b/schema/commands.schema.json index ffd7153a1..231e924a1 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -971,7 +971,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The primary working directory for the session's **default chat** — the\ndistinguished root that chat is centered on (see\n{@link ChatState.primaryWorkingDirectory}). A session has no primary of its\nown; this seeds the default chat's primary. When set, it MUST be one of\n{@link workingDirectories}. A client SHOULD supply this when the agent\nadvertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a\nhost MAY reject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's chats and their primaries)." + "description": "The primary working directory for the session's **default chat**.\n\nA session has no primary of its own — primary is a per-chat notion (see\n{@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly\ncreates the session's default chat, and there is no separate `createChat`\ncall to carry that chat's create-time fields. This field is therefore the\nonly place a client can designate the **default chat's** primary at birth;\nit is copied into that chat's read-only `primaryWorkingDirectory`. For any\nnon-default chat, pass {@link CreateChatParams.primaryWorkingDirectory}\ninstead.\n\nWhen set, it MUST be one of {@link workingDirectories}. A client SHOULD\nsupply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's chats and their primaries)." }, "fork": { "$ref": "#/$defs/SessionForkSource", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index f20f5d93b..5504cfcbd 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -5882,7 +5882,7 @@ }, "primaryWorkingDirectory": { "$ref": "#/$defs/URI", - "description": "The primary working directory for the session's **default chat** — the\ndistinguished root that chat is centered on (see\n{@link ChatState.primaryWorkingDirectory}). A session has no primary of its\nown; this seeds the default chat's primary. When set, it MUST be one of\n{@link workingDirectories}. A client SHOULD supply this when the agent\nadvertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a\nhost MAY reject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's chats and their primaries)." + "description": "The primary working directory for the session's **default chat**.\n\nA session has no primary of its own — primary is a per-chat notion (see\n{@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly\ncreates the session's default chat, and there is no separate `createChat`\ncall to carry that chat's create-time fields. This field is therefore the\nonly place a client can designate the **default chat's** primary at birth;\nit is copied into that chat's read-only `primaryWorkingDirectory`. For any\nnon-default chat, pass {@link CreateChatParams.primaryWorkingDirectory}\ninstead.\n\nWhen set, it MUST be one of {@link workingDirectories}. A client SHOULD\nsupply this when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY\nreject creation that omits it, or fall back to the first entry of\n`workingDirectories`. Ignored for forked sessions (a fork inherits the\nsource session's chats and their primaries)." }, "fork": { "$ref": "#/$defs/SessionForkSource", diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index fd5b5368b..66bc3b0aa 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -86,13 +86,21 @@ export interface CreateSessionParams extends BaseParams { */ workingDirectories?: URI[]; /** - * The primary working directory for the session's **default chat** — the - * distinguished root that chat is centered on (see - * {@link ChatState.primaryWorkingDirectory}). A session has no primary of its - * own; this seeds the default chat's primary. When set, it MUST be one of - * {@link workingDirectories}. A client SHOULD supply this when the agent - * advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a - * host MAY reject creation that omits it, or fall back to the first entry of + * The primary working directory for the session's **default chat**. + * + * A session has no primary of its own — primary is a per-chat notion (see + * {@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly + * creates the session's default chat, and there is no separate `createChat` + * call to carry that chat's create-time fields. This field is therefore the + * only place a client can designate the **default chat's** primary at birth; + * it is copied into that chat's read-only `primaryWorkingDirectory`. For any + * non-default chat, pass {@link CreateChatParams.primaryWorkingDirectory} + * instead. + * + * When set, it MUST be one of {@link workingDirectories}. A client SHOULD + * supply this when the agent advertises + * {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY + * reject creation that omits it, or fall back to the first entry of * `workingDirectories`. Ignored for forked sessions (a fork inherits the * source session's chats and their primaries). */