From 615d9a4a9dd7513bbd44b282ec3685a0de353043 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Oct 2026 14:55:03 +0200 Subject: [PATCH 01/13] chat: optional per-chat agent provider behind multipleChatProviders Let one session contain chats backed by different agents. Additive and capability-gated; absent values keep today's single-agent behavior. - InitializeResult.multipleChatProviders: host-level presence capability (not advertised on connections negotiating < 1.1.0). - CreateChatParams.provider: agent for a new chat. Absent means the source chat's agent for forks/side chats, otherwise the session's provider. Hosts reject it without the capability, for unknown agents or agents without multipleChats, and when it differs from a source chat's agent. - ChatSummary/ChatState/SessionChatSummary.provider: the chat's agent; absent means the session's provider, set whenever it differs. - moveChat: with the capability, moved chats keep their agent and report it when it differs; without it, hosts MAY reject cross-provider moves. - Per-chat config: a chat on a different agent starts from that agent's defaults instead of inheriting session values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Actions.generated.cs | 8 +++ .../Generated/Commands.generated.cs | 64 +++++++++++++++++++ .../JsonSerializerContext.generated.cs | 1 + .../Generated/Notifications.generated.cs | 4 +- .../Generated/State.generated.cs | 50 ++++++++++++++- clients/go/ahptypes/commands.generated.go | 59 +++++++++++++++++ .../go/ahptypes/notifications.generated.go | 4 +- clients/go/ahptypes/state.generated.go | 42 +++++++++++- .../generated/Actions.generated.kt | 8 +++ .../generated/Commands.generated.kt | 59 ++++++++++++++++- .../generated/Notifications.generated.kt | 4 +- .../generated/State.generated.kt | 41 +++++++++++- clients/rust/crates/ahp-types/src/actions.rs | 7 ++ clients/rust/crates/ahp-types/src/commands.rs | 62 ++++++++++++++++++ .../crates/ahp-types/src/notifications.rs | 4 +- clients/rust/crates/ahp-types/src/state.rs | 45 ++++++++++++- .../Generated/Actions.generated.swift | 8 +++ .../Generated/Commands.generated.swift | 64 ++++++++++++++++++- .../Generated/Notifications.generated.swift | 4 +- .../Generated/State.generated.swift | 42 +++++++++++- docs/.changes/20261019-chat-provider.json | 4 ++ docs/guide/state-model.md | 9 +++ docs/specification/chat-channel.md | 48 ++++++++++++++ schema/actions.schema.json | 24 +++++-- schema/commands.schema.json | 39 +++++++++-- schema/errors.schema.json | 39 +++++++++-- schema/notifications.schema.json | 22 +++++-- schema/state.schema.json | 20 ++++-- scripts/generate-csharp.ts | 1 + scripts/generate-go.ts | 1 + scripts/generate-kotlin.ts | 1 + scripts/generate-rust.ts | 1 + scripts/generate-swift.ts | 1 + types/channels-chat/commands.ts | 36 +++++++++++ types/channels-chat/state.ts | 28 ++++++++ types/channels-session/state.ts | 15 ++++- types/common/commands.ts | 28 ++++++++ .../round-trips/076-create-chat-provider.json | 18 ++++++ ...77-initialize-multiple-chat-providers.json | 20 ++++++ .../078-chat-summary-provider.json | 28 ++++++++ 40 files changed, 919 insertions(+), 44 deletions(-) create mode 100644 docs/.changes/20261019-chat-provider.json create mode 100644 types/test-cases/round-trips/076-create-chat-provider.json create mode 100644 types/test-cases/round-trips/077-initialize-multiple-chat-providers.json create mode 100644 types/test-cases/round-trips/078-chat-summary-provider.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index 529eb8d6b..5c81dc828 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -2781,6 +2781,14 @@ public sealed record PartialChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; init; } + /// Agent provider ID backing this chat. Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}. + /// See {@link ChatState.provider} for the full semantics. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Provider { get; init; } + /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 01840ca71..d54257776 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -436,6 +436,22 @@ public sealed record InitializeResult /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public TcpConnectionsCapability? TcpConnections { get; init; } + + /// Present when the host lets chats in one session be backed by different + /// agents. Clients MAY then pass {@link CreateChatParams.provider} to + /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that + /// differs from the session's {@link SessionMetadata.provider}. When absent, + /// every chat uses its session's provider and clients MUST NOT send + /// {@link CreateChatParams.provider}. + /// + /// This is a host capability rather than an {@link AgentCapabilities | agent + /// capability} because the host, not an individual agent, decides which + /// agents may share a session. Hosts MUST NOT advertise it on connections + /// that negotiated a protocol version earlier than `1.1.0`. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MultipleChatProvidersCapability? MultipleChatProviders { get; init; } } /// Identifies a protocol implementation — the software (and build) on one end @@ -565,6 +581,17 @@ public sealed record AutomationCustomizationsCapability { } +/// Present when the host supports chats backed by different agents within one +/// session. See {@link InitializeResult.multipleChatProviders}. +/// +/// The empty object means "supported"; sub-fields are reserved for future +/// options. +/// +/// Stability: 1.0 - Early development. +public sealed record MultipleChatProvidersCapability +{ +} + /// Re-establishes a dropped connection. The server replays missed actions or /// provides fresh snapshots. public sealed record ReconnectParams @@ -966,6 +993,33 @@ public sealed record CreateChatParams /// Client-selected initial display title for the new chat. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Title { get; init; } + + /// Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. + /// When absent, the chat uses the source chat's provider if `source` is + /// present, and otherwise the session's + /// {@link SessionMetadata.provider | `provider`}. + /// + /// A client MUST NOT supply this field unless the host advertises + /// {@link InitializeResult.multipleChatProviders}. The host MUST reject the + /// request with `InvalidParams` when: + /// + /// - it does not advertise `multipleChatProviders`; + /// - the value is not the `provider` of an agent in the root state's + /// `agents`, or that agent does not advertise + /// {@link AgentCapabilities.multipleChats}; or + /// - `source` is present and the value differs from the source chat's + /// provider. Forks and side chats always use their source chat's agent; + /// history is never transferred between agents. + /// + /// Capability checks that depend on the agent — `multipleChats.fork`, + /// `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the + /// chat's agent. The new chat's + /// {@link ChatSummary.provider | `ChatSummary.provider`} reports the + /// resolved agent. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Provider { get; init; } } /// Disposes a chat and cleans up server-side resources. @@ -1030,6 +1084,16 @@ public sealed record ChatMoveToNewSessionDestination /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// +/// When the host advertises {@link InitializeResult.multipleChatProviders}, +/// every moved chat keeps its agent ({@link ChatSummary.provider}), and every +/// moved chat whose provider differs from the destination session's +/// {@link SessionMetadata.provider} MUST carry `provider` in its destination +/// catalog entry. A `newSession` destination's session `provider` is the +/// requested chat's provider. A host that does not advertise the capability +/// cannot report a per-chat provider, so it MAY reject, as an unsupported +/// destination, a move that would place a chat in a session with a different +/// provider. +/// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index a4b3b5c8c..ff8271a59 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -283,6 +283,7 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(ModelSelection))] [JsonSerializable(typeof(MoveChatParams))] [JsonSerializable(typeof(MoveChatResult))] +[JsonSerializable(typeof(MultipleChatProvidersCapability))] [JsonSerializable(typeof(MultipleChatsCapability))] [JsonSerializable(typeof(MultipleWorkingDirectoriesCapability))] [JsonSerializable(typeof(OtlpExportLogsParams))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs index 15577ffa8..3e65eae73 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs @@ -410,7 +410,9 @@ public sealed record OtlpExportMetricsParams /// Partial equivalent of SessionSummary — every field is optional for delta updates. public sealed record PartialSessionSummary { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Provider { get; init; } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 9d5ab51ce..3fd4e2e06 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2365,6 +2365,14 @@ public sealed class ChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; set; } + /// Agent provider ID backing this chat. Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}. + /// See {@link ChatState.provider} for the full semantics. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Provider { get; set; } + /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2492,6 +2500,21 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; set; } + /// Agent provider ID (an {@link AgentInfo.provider}) backing this chat. + /// + /// Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an + /// absent value that way. Hosts MAY omit it when it equals the session's + /// provider and MUST set it when it differs, which only happens when the + /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's + /// agent never changes; hosts MUST NOT change this value through + /// `session/chatUpdated`. Forks and side chats always use their source + /// chat's agent. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Provider { get; set; } + /// 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 @@ -2910,7 +2933,9 @@ public sealed record ChatInputSkipped /// `root/sessionSummaryChanged`. public sealed class SessionState { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. public required string Provider { get; set; } /// Session title @@ -3248,7 +3273,9 @@ public sealed record SessionToolAuthenticationRequest /// carries multiple chats. public sealed class SessionSummary { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. public required string Provider { get; set; } /// Session title @@ -3343,6 +3370,16 @@ public sealed record SessionChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; init; } + /// Agent provider ID backing this chat, matching {@link ChatSummary.provider}. + /// + /// Absence means the session's {@link SessionMetadata.provider | `provider`}. + /// Hosts MUST set it when it differs, so session lists can show sessions + /// whose chats use different agents without subscribing to them. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Provider { get; init; } + /// Current chat status, matching {@link ChatSummary.status}. /// /// Includes the activity bits and the orthogonal {@link SessionStatus.IsRead} @@ -3503,7 +3540,14 @@ public sealed class SessionConfigState /// It carries only the chat's own values; the session's config schema /// applies. The effective config of a chat is the session's values with /// these values applied over them. Only properties with `chatMutable: true` -/// in the session's config schema can be set on a chat. +/// in the session's config schema can be set on a chat. +/// +/// A chat whose {@link ChatState.provider | `provider`} differs from the +/// session's provider does not inherit the session's values: the session's +/// agent owns those. The host seeds that chat's values from its agent's +/// defaults when it creates the chat, and they are the chat's effective +/// config. The chat's settings are its own and change only through +/// `chat/configChanged` on that chat. public sealed class ChatConfigState { /// Config values set on this chat, keyed by property id diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index aab1cd030..baa77b8f1 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -224,6 +224,20 @@ type InitializeResult struct { // // Stability: 1.0 - Early development. TcpConnections *TcpConnectionsCapability `json:"tcpConnections,omitempty"` + // Present when the host lets chats in one session be backed by different + // agents. Clients MAY then pass {@link CreateChatParams.provider} to + // `createChat`, and chats MAY report a {@link ChatSummary.provider} that + // differs from the session's {@link SessionMetadata.provider}. When absent, + // every chat uses its session's provider and clients MUST NOT send + // {@link CreateChatParams.provider}. + // + // This is a host capability rather than an {@link AgentCapabilities | agent + // capability} because the host, not an individual agent, decides which + // agents may share a session. Hosts MUST NOT advertise it on connections + // that negotiated a protocol version earlier than `1.1.0`. + // + // Stability: 1.0 - Early development. + MultipleChatProviders *MultipleChatProvidersCapability `json:"multipleChatProviders,omitempty"` } // Optional capabilities a client declares during `initialize`. @@ -307,6 +321,16 @@ type AutomationRunCancellationCapability struct { type AutomationCustomizationsCapability struct { } +// Present when the host supports chats backed by different agents within one +// session. See {@link InitializeResult.multipleChatProviders}. +// +// The empty object means "supported"; sub-fields are reserved for future +// options. +// +// Stability: 1.0 - Early development. +type MultipleChatProvidersCapability struct { +} + // Identifies a protocol implementation — the software (and build) on one end // of the connection, as distinct from the {@link AgentInfo | agent persona} it // hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the @@ -645,6 +669,31 @@ type CreateChatParams struct { WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Client-selected initial display title for the new chat. Title *string `json:"title,omitempty"` + // Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. + // When absent, the chat uses the source chat's provider if `source` is + // present, and otherwise the session's + // {@link SessionMetadata.provider | `provider`}. + // + // A client MUST NOT supply this field unless the host advertises + // {@link InitializeResult.multipleChatProviders}. The host MUST reject the + // request with `InvalidParams` when: + // + // - it does not advertise `multipleChatProviders`; + // - the value is not the `provider` of an agent in the root state's + // `agents`, or that agent does not advertise + // {@link AgentCapabilities.multipleChats}; or + // - `source` is present and the value differs from the source chat's + // provider. Forks and side chats always use their source chat's agent; + // history is never transferred between agents. + // + // Capability checks that depend on the agent — `multipleChats.fork`, + // `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the + // chat's agent. The new chat's + // {@link ChatSummary.provider | `ChatSummary.provider`} reports the + // resolved agent. + // + // Stability: 1.0 - Early development. + Provider *string `json:"provider,omitempty"` } // Disposes a chat and cleans up server-side resources. @@ -700,6 +749,16 @@ type ChatMoveToNewSessionDestination struct { // Rejection leaves ownership, catalog order, chat state, and root summaries // unchanged. // +// When the host advertises {@link InitializeResult.multipleChatProviders}, +// every moved chat keeps its agent ({@link ChatSummary.provider}), and every +// moved chat whose provider differs from the destination session's +// {@link SessionMetadata.provider} MUST carry `provider` in its destination +// catalog entry. A `newSession` destination's session `provider` is the +// requested chat's provider. A host that does not advertise the capability +// cannot report a per-chat provider, so it MAY reject, as an unsupported +// destination, a move that would place a chat in a session with a different +// provider. +// // On success every moved chat keeps its URI, state, and immutable // `ChatOrigin`. The host commits ownership and catalog order before publishing // `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 973e1577c..858924f93 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -336,7 +336,9 @@ type OtlpExportMetricsParams struct { // PartialSessionSummary is the partial equivalent of SessionSummary — every field is optional for delta updates. type PartialSessionSummary struct { - // Agent provider ID + // Agent provider ID of the session: the agent that backs the session's + // initial chat and every chat that does not report its own + // {@link ChatSummary.provider | `provider`}. Provider *string `json:"provider,omitempty"` // Session title Title *string `json:"title,omitempty"` diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index ed37ca904..05ca7238b 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -977,7 +977,9 @@ type ConfigSchema struct { // the root channel; the host keeps the two in sync via // `root/sessionSummaryChanged`. type SessionState struct { - // Agent provider ID + // Agent provider ID of the session: the agent that backs the session's + // initial chat and every chat that does not report its own + // {@link ChatSummary.provider | `provider`}. Provider string `json:"provider"` // Session title Title string `json:"title"` @@ -1256,7 +1258,9 @@ type SessionToolAuthenticationRequest struct { // values pass through unchanged). The rules only matter once a session // carries multiple chats. type SessionSummary struct { - // Agent provider ID + // Agent provider ID of the session: the agent that backs the session's + // initial chat and every chat that does not report its own + // {@link ChatSummary.provider | `provider`}. Provider string `json:"provider"` // Session title Title string `json:"title"` @@ -1320,6 +1324,14 @@ type SessionChatSummary struct { // read-only chats. Absence defaults to {@link ChatInteractivity.Full} for // backward compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` + // Agent provider ID backing this chat, matching {@link ChatSummary.provider}. + // + // Absence means the session's {@link SessionMetadata.provider | `provider`}. + // Hosts MUST set it when it differs, so session lists can show sessions + // whose chats use different agents without subscribing to them. + // + // Stability: 1.0 - Early development. + Provider *string `json:"provider,omitempty"` // Current chat status, matching {@link ChatSummary.status}. // // Includes the activity bits and the orthogonal {@link SessionStatus.IsRead} @@ -1392,6 +1404,19 @@ type ChatState struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` + // Agent provider ID (an {@link AgentInfo.provider}) backing this chat. + // + // Absence means the owning session's + // {@link SessionMetadata.provider | `provider`}; clients MUST treat an + // absent value that way. Hosts MAY omit it when it equals the session's + // provider and MUST set it when it differs, which only happens when the + // host advertises {@link InitializeResult.multipleChatProviders}. A chat's + // agent never changes; hosts MUST NOT change this value through + // `session/chatUpdated`. Forks and side chats always use their source + // chat's agent. + // + // Stability: 1.0 - Early development. + Provider *string `json:"provider,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 @@ -1535,6 +1560,12 @@ type ChatSummary struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` + // Agent provider ID backing this chat. Absence means the owning session's + // {@link SessionMetadata.provider | `provider`}. + // See {@link ChatState.provider} for the full semantics. + // + // Stability: 1.0 - Early development. + Provider *string `json:"provider,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"` @@ -1699,6 +1730,13 @@ type SessionConfigState struct { // applies. The effective config of a chat is the session's values with // these values applied over them. Only properties with `chatMutable: true` // in the session's config schema can be set on a chat. +// +// A chat whose {@link ChatState.provider | `provider`} differs from the +// session's provider does not inherit the session's values: the session's +// agent owns those. The host seeds that chat's values from its agent's +// defaults when it creates the chat, and they are the chat's effective +// config. The chat's settings are its own and change only through +// `chat/configChanged` on that chat. type ChatConfigState struct { // Config values set on this chat, keyed by property id Values map[string]json.RawMessage `json:"values"` 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 467a7f67b..f4124ee0a 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 @@ -1686,6 +1686,14 @@ data class PartialChatSummary( * compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID backing this chat. Absence means the owning session's + * {@link SessionMetadata.provider | `provider`}. + * See {@link ChatState.provider} for the full semantics. + * + * Stability: 1.0 - Early development. + */ + val provider: String? = null, /** * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. 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 b337c6fa4..353d42687 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 @@ -466,7 +466,23 @@ data class InitializeResult( * * Stability: 1.0 - Early development. */ - val tcpConnections: TcpConnectionsCapability? = null + val tcpConnections: TcpConnectionsCapability? = null, + /** + * Present when the host lets chats in one session be backed by different + * agents. Clients MAY then pass {@link CreateChatParams.provider} to + * `createChat`, and chats MAY report a {@link ChatSummary.provider} that + * differs from the session's {@link SessionMetadata.provider}. When absent, + * every chat uses its session's provider and clients MUST NOT send + * {@link CreateChatParams.provider}. + * + * This is a host capability rather than an {@link AgentCapabilities | agent + * capability} because the host, not an individual agent, decides which + * agents may share a session. Hosts MUST NOT advertise it on connections + * that negotiated a protocol version earlier than `1.1.0`. + * + * Stability: 1.0 - Early development. + */ + val multipleChatProviders: MultipleChatProvidersCapability? = null ) @Serializable @@ -534,6 +550,18 @@ class AutomationRunCancellationCapability @Serializable class AutomationCustomizationsCapability +/** + * Present when the host supports chats backed by different agents within one + * session. See {@link InitializeResult.multipleChatProviders}. + * + * The empty object means "supported"; sub-fields are reserved for future + * options. + * + * Stability: 1.0 - Early development. + */ +@Serializable +class MultipleChatProvidersCapability + @Serializable data class Implementation( /** @@ -904,7 +932,34 @@ data class CreateChatParams( /** * Client-selected initial display title for the new chat. */ - val title: String? = null + val title: String? = null, + /** + * Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. + * When absent, the chat uses the source chat's provider if `source` is + * present, and otherwise the session's + * {@link SessionMetadata.provider | `provider`}. + * + * A client MUST NOT supply this field unless the host advertises + * {@link InitializeResult.multipleChatProviders}. The host MUST reject the + * request with `InvalidParams` when: + * + * - it does not advertise `multipleChatProviders`; + * - the value is not the `provider` of an agent in the root state's + * `agents`, or that agent does not advertise + * {@link AgentCapabilities.multipleChats}; or + * - `source` is present and the value differs from the source chat's + * provider. Forks and side chats always use their source chat's agent; + * history is never transferred between agents. + * + * Capability checks that depend on the agent — `multipleChats.fork`, + * `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the + * chat's agent. The new chat's + * {@link ChatSummary.provider | `ChatSummary.provider`} reports the + * resolved agent. + * + * Stability: 1.0 - Early development. + */ + val provider: 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 761e7513f..3e29687ec 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 @@ -356,7 +356,9 @@ data class OtlpExportMetricsParams( @Serializable data class PartialSessionSummary( /** - * Agent provider ID + * Agent provider ID of the session: the agent that backs the session's + * initial chat and every chat that does not report its own + * {@link ChatSummary.provider | `provider`}. */ val provider: String? = 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 84eca910f..103539837 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 @@ -1811,6 +1811,21 @@ data class ChatState( * compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID (an {@link AgentInfo.provider}) backing this chat. + * + * Absence means the owning session's + * {@link SessionMetadata.provider | `provider`}; clients MUST treat an + * absent value that way. Hosts MAY omit it when it equals the session's + * provider and MUST set it when it differs, which only happens when the + * host advertises {@link InitializeResult.multipleChatProviders}. A chat's + * agent never changes; hosts MUST NOT change this value through + * `session/chatUpdated`. Forks and side chats always use their source + * chat's agent. + * + * Stability: 1.0 - Early development. + */ + val provider: String? = null, /** * The subset of the session's * {@link SessionState.workingDirectories | `workingDirectories`} that this @@ -1959,6 +1974,14 @@ data class ChatSummary( * compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID backing this chat. Absence means the owning session's + * {@link SessionMetadata.provider | `provider`}. + * See {@link ChatState.provider} for the full semantics. + * + * Stability: 1.0 - Early development. + */ + val provider: String? = null, /** * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. @@ -1987,7 +2010,9 @@ data class SideChatSelection( @Serializable data class SessionState( /** - * Agent provider ID + * Agent provider ID of the session: the agent that backs the session's + * initial chat and every chat that does not report its own + * {@link ChatSummary.provider | `provider`}. */ val provider: String, /** @@ -2382,7 +2407,9 @@ data class SessionToolAuthenticationRequest( @Serializable data class SessionSummary( /** - * Agent provider ID + * Agent provider ID of the session: the agent that backs the session's + * initial chat and every chat that does not report its own + * {@link ChatSummary.provider | `provider`}. */ val provider: String, /** @@ -2484,6 +2511,16 @@ data class SessionChatSummary( * backward compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID backing this chat, matching {@link ChatSummary.provider}. + * + * Absence means the session's {@link SessionMetadata.provider | `provider`}. + * Hosts MUST set it when it differs, so session lists can show sessions + * whose chats use different agents without subscribing to them. + * + * Stability: 1.0 - Early development. + */ + val provider: String? = null, /** * Current chat status, matching {@link ChatSummary.status}. * diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 49074e354..96a41ecf0 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -2395,6 +2395,13 @@ pub struct PartialChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID backing this chat. Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}. + /// See {@link ChatState.provider} for the full semantics. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: 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")] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 7834cf264..c8d95118c 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -369,6 +369,21 @@ pub struct InitializeResult { /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] pub tcp_connections: Option, + /// Present when the host lets chats in one session be backed by different + /// agents. Clients MAY then pass {@link CreateChatParams.provider} to + /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that + /// differs from the session's {@link SessionMetadata.provider}. When absent, + /// every chat uses its session's provider and clients MUST NOT send + /// {@link CreateChatParams.provider}. + /// + /// This is a host capability rather than an {@link AgentCapabilities | agent + /// capability} because the host, not an individual agent, decides which + /// agents may share a session. Hosts MUST NOT advertise it on connections + /// that negotiated a protocol version earlier than `1.1.0`. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub multiple_chat_providers: Option, } /// Optional capabilities a client declares during `initialize`. @@ -468,6 +483,17 @@ pub struct AutomationRunCancellationCapability {} #[serde(rename_all = "camelCase")] pub struct AutomationCustomizationsCapability {} +/// Present when the host supports chats backed by different agents within one +/// session. See {@link InitializeResult.multipleChatProviders}. +/// +/// The empty object means "supported"; sub-fields are reserved for future +/// options. +/// +/// Stability: 1.0 - Early development. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MultipleChatProvidersCapability {} + /// Identifies a protocol implementation — the software (and build) on one end /// of the connection, as distinct from the {@link AgentInfo | agent persona} it /// hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the @@ -899,6 +925,32 @@ pub struct CreateChatParams { /// Client-selected initial display title for the new chat. #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, + /// Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. + /// When absent, the chat uses the source chat's provider if `source` is + /// present, and otherwise the session's + /// {@link SessionMetadata.provider | `provider`}. + /// + /// A client MUST NOT supply this field unless the host advertises + /// {@link InitializeResult.multipleChatProviders}. The host MUST reject the + /// request with `InvalidParams` when: + /// + /// - it does not advertise `multipleChatProviders`; + /// - the value is not the `provider` of an agent in the root state's + /// `agents`, or that agent does not advertise + /// {@link AgentCapabilities.multipleChats}; or + /// - `source` is present and the value differs from the source chat's + /// provider. Forks and side chats always use their source chat's agent; + /// history is never transferred between agents. + /// + /// Capability checks that depend on the agent — `multipleChats.fork`, + /// `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the + /// chat's agent. The new chat's + /// {@link ChatSummary.provider | `ChatSummary.provider`} reports the + /// resolved agent. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, } /// Disposes a chat and cleans up server-side resources. @@ -957,6 +1009,16 @@ pub struct ChatMoveToNewSessionDestination {} /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// +/// When the host advertises {@link InitializeResult.multipleChatProviders}, +/// every moved chat keeps its agent ({@link ChatSummary.provider}), and every +/// moved chat whose provider differs from the destination session's +/// {@link SessionMetadata.provider} MUST carry `provider` in its destination +/// catalog entry. A `newSession` destination's session `provider` is the +/// requested chat's provider. A host that does not advertise the capability +/// cannot report a per-chat provider, so it MAY reject, as an unsupported +/// destination, a move that would place a chat in a session with a different +/// provider. +/// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 113974cda..ef0c4d49e 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -403,7 +403,9 @@ pub struct OtlpExportMetricsParams { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct PartialSessionSummary { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, /// Session title diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 3c547826d..3537011c0 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2063,6 +2063,20 @@ pub struct ChatState { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID (an {@link AgentInfo.provider}) backing this chat. + /// + /// Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an + /// absent value that way. Hosts MAY omit it when it equals the session's + /// provider and MUST set it when it differs, which only happens when the + /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's + /// agent never changes; hosts MUST NOT change this value through + /// `session/chatUpdated`. Forks and side chats always use their source + /// chat's agent. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: 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 @@ -2232,6 +2246,13 @@ pub struct ChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID backing this chat. Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}. + /// See {@link ChatState.provider} for the full semantics. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: 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")] @@ -2320,7 +2341,9 @@ pub struct SideChatSelection { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionState { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. pub provider: String, /// Session title pub title: String, @@ -2659,7 +2682,9 @@ pub struct SessionToolAuthenticationRequest { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionSummary { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. pub provider: String, /// Session title pub title: String, @@ -2736,6 +2761,15 @@ pub struct SessionChatSummary { /// backward compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID backing this chat, matching {@link ChatSummary.provider}. + /// + /// Absence means the session's {@link SessionMetadata.provider | `provider`}. + /// Hosts MUST set it when it differs, so session lists can show sessions + /// whose chats use different agents without subscribing to them. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, /// Current chat status, matching {@link ChatSummary.status}. /// /// Includes the activity bits and the orthogonal {@link SessionStatus.IsRead} @@ -2879,6 +2913,13 @@ pub struct SessionConfigState { /// applies. The effective config of a chat is the session's values with /// these values applied over them. Only properties with `chatMutable: true` /// in the session's config schema can be set on a chat. +/// +/// A chat whose {@link ChatState.provider | `provider`} differs from the +/// session's provider does not inherit the session's values: the session's +/// agent owns those. The host seeds that chat's values from its agent's +/// defaults when it creates the chat, and they are the chat's effective +/// config. The chat's settings are its own and change only through +/// `chat/configChanged` on that chat. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 14a77834d..9c0cad0d9 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -2566,6 +2566,12 @@ public struct PartialChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? + /// Agent provider ID backing this chat. Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}. + /// See {@link ChatState.provider} for the full semantics. + /// + /// Stability: 1.0 - Early development. + public var provider: String? /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. public var workingDirectories: [String]? @@ -2580,6 +2586,7 @@ public struct PartialChatSummary: Codable, Sendable { origin: ChatOrigin? = nil, movable: Bool? = nil, interactivity: ChatInteractivity? = nil, + provider: String? = nil, workingDirectories: [String]? = nil ) { self.resource = resource @@ -2591,6 +2598,7 @@ public struct PartialChatSummary: Codable, Sendable { self.origin = origin self.movable = movable self.interactivity = interactivity + self.provider = provider 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 2f41e820e..dc63fbb09 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -423,6 +423,20 @@ public struct InitializeResult: Codable, Sendable { /// /// Stability: 1.0 - Early development. public var tcpConnections: TcpConnectionsCapability? + /// Present when the host lets chats in one session be backed by different + /// agents. Clients MAY then pass {@link CreateChatParams.provider} to + /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that + /// differs from the session's {@link SessionMetadata.provider}. When absent, + /// every chat uses its session's provider and clients MUST NOT send + /// {@link CreateChatParams.provider}. + /// + /// This is a host capability rather than an {@link AgentCapabilities | agent + /// capability} because the host, not an individual agent, decides which + /// agents may share a session. Hosts MUST NOT advertise it on connections + /// that negotiated a protocol version earlier than `1.1.0`. + /// + /// Stability: 1.0 - Early development. + public var multipleChatProviders: MultipleChatProvidersCapability? enum CodingKeys: String, CodingKey { case protocolVersion @@ -436,6 +450,7 @@ public struct InitializeResult: Codable, Sendable { case telemetry case automations case tcpConnections + case multipleChatProviders } public init( @@ -449,7 +464,8 @@ public struct InitializeResult: Codable, Sendable { terminalCommandPrefix: String? = nil, telemetry: TelemetryCapabilities? = nil, automations: AutomationCapabilities? = nil, - tcpConnections: TcpConnectionsCapability? = nil + tcpConnections: TcpConnectionsCapability? = nil, + multipleChatProviders: MultipleChatProvidersCapability? = nil ) { self.protocolVersion = protocolVersion self.serverSeq = serverSeq @@ -462,6 +478,7 @@ public struct InitializeResult: Codable, Sendable { self.telemetry = telemetry self.automations = automations self.tcpConnections = tcpConnections + self.multipleChatProviders = multipleChatProviders } } @@ -554,6 +571,21 @@ public struct AutomationCustomizationsCapability: Codable, Sendable { } } +/// Present when the host supports chats backed by different agents within one +/// session. See {@link InitializeResult.multipleChatProviders}. +/// +/// The empty object means "supported"; sub-fields are reserved for future +/// options. +/// +/// Stability: 1.0 - Early development. +public struct MultipleChatProvidersCapability: Codable, Sendable { + + public init( + + ) { + } +} + public struct Implementation: Codable, Sendable { /// Implementation name, e.g. a product or package identifier. public var name: String @@ -975,6 +1007,31 @@ public struct CreateChatParams: Codable, Sendable { public var workingDirectories: [String]? /// Client-selected initial display title for the new chat. public var title: String? + /// Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. + /// When absent, the chat uses the source chat's provider if `source` is + /// present, and otherwise the session's + /// {@link SessionMetadata.provider | `provider`}. + /// + /// A client MUST NOT supply this field unless the host advertises + /// {@link InitializeResult.multipleChatProviders}. The host MUST reject the + /// request with `InvalidParams` when: + /// + /// - it does not advertise `multipleChatProviders`; + /// - the value is not the `provider` of an agent in the root state's + /// `agents`, or that agent does not advertise + /// {@link AgentCapabilities.multipleChats}; or + /// - `source` is present and the value differs from the source chat's + /// provider. Forks and side chats always use their source chat's agent; + /// history is never transferred between agents. + /// + /// Capability checks that depend on the agent — `multipleChats.fork`, + /// `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the + /// chat's agent. The new chat's + /// {@link ChatSummary.provider | `ChatSummary.provider`} reports the + /// resolved agent. + /// + /// Stability: 1.0 - Early development. + public var provider: String? enum CodingKeys: String, CodingKey { case channel @@ -984,6 +1041,7 @@ public struct CreateChatParams: Codable, Sendable { case source case workingDirectories case title + case provider } public init( @@ -993,7 +1051,8 @@ public struct CreateChatParams: Codable, Sendable { initialMessage: Message? = nil, source: ChatSource? = nil, workingDirectories: [String]? = nil, - title: String? = nil + title: String? = nil, + provider: String? = nil ) { self.channel = channel self.meta = meta @@ -1002,6 +1061,7 @@ public struct CreateChatParams: Codable, Sendable { self.source = source self.workingDirectories = workingDirectories self.title = title + self.provider = provider } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index a1155b2e1..444276909 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -367,7 +367,9 @@ public struct OtlpExportMetricsParams: Codable, Sendable { // MARK: - Partial Summary Types public struct PartialSessionSummary: Codable, Sendable { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. public var provider: String? /// Session title public var title: String? diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 5987eed86..2926fb4ea 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1822,6 +1822,19 @@ public struct ChatState: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? + /// Agent provider ID (an {@link AgentInfo.provider}) backing this chat. + /// + /// Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an + /// absent value that way. Hosts MAY omit it when it equals the session's + /// provider and MUST set it when it differs, which only happens when the + /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's + /// agent never changes; hosts MUST NOT change this value through + /// `session/chatUpdated`. Forks and side chats always use their source + /// chat's agent. + /// + /// Stability: 1.0 - Early development. + public var provider: 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 @@ -1907,6 +1920,7 @@ public struct ChatState: Codable, Sendable { case origin case movable case interactivity + case provider case workingDirectories case changesets case backgroundWork @@ -1931,6 +1945,7 @@ public struct ChatState: Codable, Sendable { origin: ChatOrigin? = nil, movable: Bool? = nil, interactivity: ChatInteractivity? = nil, + provider: String? = nil, workingDirectories: [String]? = nil, changesets: [Changeset]? = nil, backgroundWork: [BackgroundWork]? = nil, @@ -1953,6 +1968,7 @@ public struct ChatState: Codable, Sendable { self.origin = origin self.movable = movable self.interactivity = interactivity + self.provider = provider self.workingDirectories = workingDirectories self.changesets = changesets self.backgroundWork = backgroundWork @@ -1996,6 +2012,12 @@ public struct ChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? + /// Agent provider ID backing this chat. Absence means the owning session's + /// {@link SessionMetadata.provider | `provider`}. + /// See {@link ChatState.provider} for the full semantics. + /// + /// Stability: 1.0 - Early development. + public var provider: String? /// The subset of the session's working directories this chat uses. /// See {@link ChatState.workingDirectories} for the full semantics. public var workingDirectories: [String]? @@ -2010,6 +2032,7 @@ public struct ChatSummary: Codable, Sendable { origin: ChatOrigin? = nil, movable: Bool? = nil, interactivity: ChatInteractivity? = nil, + provider: String? = nil, workingDirectories: [String]? = nil ) { self.resource = resource @@ -2021,6 +2044,7 @@ public struct ChatSummary: Codable, Sendable { self.origin = origin self.movable = movable self.interactivity = interactivity + self.provider = provider self.workingDirectories = workingDirectories } } @@ -2047,7 +2071,9 @@ public struct SideChatSelection: Codable, Sendable { } public struct SessionState: Codable, Sendable { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. public var provider: String /// Session title public var title: String @@ -2472,7 +2498,9 @@ public struct SessionToolAuthenticationRequest: Codable, Sendable { } public struct SessionSummary: Codable, Sendable { - /// Agent provider ID + /// Agent provider ID of the session: the agent that backs the session's + /// initial chat and every chat that does not report its own + /// {@link ChatSummary.provider | `provider`}. public var provider: String /// Session title public var title: String @@ -2587,6 +2615,14 @@ public struct SessionChatSummary: Codable, Sendable { /// read-only chats. Absence defaults to {@link ChatInteractivity.Full} for /// backward compatibility. public var interactivity: ChatInteractivity? + /// Agent provider ID backing this chat, matching {@link ChatSummary.provider}. + /// + /// Absence means the session's {@link SessionMetadata.provider | `provider`}. + /// Hosts MUST set it when it differs, so session lists can show sessions + /// whose chats use different agents without subscribing to them. + /// + /// Stability: 1.0 - Early development. + public var provider: String? /// Current chat status, matching {@link ChatSummary.status}. /// /// Includes the activity bits and the orthogonal {@link SessionStatus.IsRead} @@ -2608,6 +2644,7 @@ public struct SessionChatSummary: Codable, Sendable { title: String, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, + provider: String? = nil, status: SessionStatus? = nil, changes: ChangesSummary? = nil ) { @@ -2615,6 +2652,7 @@ public struct SessionChatSummary: Codable, Sendable { self.title = title self.origin = origin self.interactivity = interactivity + self.provider = provider self.status = status self.changes = changes } diff --git a/docs/.changes/20261019-chat-provider.json b/docs/.changes/20261019-chat-provider.json new file mode 100644 index 000000000..a43c86209 --- /dev/null +++ b/docs/.changes/20261019-chat-provider.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Optional per-chat agent: `CreateChatParams.provider` and `ChatSummary.provider` (mirrored on `ChatState` and `SessionChatSummary`), gated by the host-level `InitializeResult.multipleChatProviders` capability, let one session contain chats backed by different agents. Absent `provider` means the session's provider; forks and side chats keep their source chat's agent." +} diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 296b55d54..dcb5efa56 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -192,6 +192,7 @@ ChatState { activity?: string modifiedAt: string origin?: ChatOrigin // how the chat came to exist (user / fork / sideChat / tool) + provider?: string // chat's agent; absent = session's provider workingDirectories?: URI[] // subset of session's workingDirectories changesets?: Changeset[] // per-chat Branch, Uncommitted Changes, etc. config?: ChatConfigState // config values set on this chat (overlay) @@ -219,6 +220,14 @@ properties marked `chatMutable: true` in the session's schema can be set per chat, with `chat/configChanged`. See [Per-chat configuration](/specification/chat-channel#per-chat-configuration). +`provider` names the chat's agent. Absence means the session's `provider`. +Hosts that advertise `multipleChatProviders` in their `initialize` result may +back chats in one session with different agents, chosen by `createChat`'s +`provider`; forks and side chats keep their source chat's agent. A chat whose +agent differs from the session's starts from that agent's config defaults +rather than inheriting the session's values. See +[Chat providers](/specification/chat-channel#chat-providers). + Fork and side-chat creation both reference source turns by stable identifiers. Both source forms are fully discriminated — `{ kind: 'fork', chat, turnId }` and `{ kind: 'sideChat', chat, turnId, selection? }`. For side chats, hosts diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index c52652f5a..339afd245 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -93,6 +93,44 @@ To reset a property to the session's value, replace the overlay with one that omits the property. Every other property is changed on the session with `session/configChanged`. +A chat backed by a different agent than its session (see +[Chat providers](#chat-providers)) does not inherit the session's values, +which belong to the session's agent. The host seeds that chat's +`ChatState.config` from its own agent's defaults when it creates the chat, and +those values are the chat's effective config. The chat's settings are its own +and change only through `chat/configChanged` on that chat. + +### Chat providers + +Every chat is backed by one agent, identified by its +[`AgentInfo.provider`](/reference/root#agentinfo). +[`SessionState.provider`](/reference/session#sessionmetadata) is the session's +agent: the agent of the session's initial chat and the default for every other +chat. A host that advertises +[`multipleChatProviders`](/reference/common#initializeresult) in its +`initialize` result lets one session contain chats backed by different agents. + +- [`ChatSummary.provider`](/reference/chat#chatsummary) (mirrored on + `ChatState` and on [`SessionChatSummary`](/reference/session#sessionchatsummary)) + reports a chat's agent. Absence means the session's `provider`; clients MUST + treat it that way. Hosts MAY omit it when it equals the session's provider + and MUST set it when it differs. A chat's agent never changes. +- A client MAY pass [`provider`](/reference/chat#createchatparams) to + `createChat` only when the host advertises `multipleChatProviders`. When it is + absent, the new chat uses the source chat's agent for forks and side chats, + and the session's agent otherwise. +- The host MUST reject `createChat` with `InvalidParams` when `provider` is sent + without the capability, names an agent that is not in the root state's + `agents`, names an agent that does not advertise `multipleChats`, or differs + from the source chat's agent when `source` is present. Forks and side chats + always keep the source chat's agent; history is never transferred between + agents. +- Agent capability checks (`multipleChats.fork`, `multipleChats.sideChat`, + `multipleWorkingDirectories`) use the chat's agent. +- Hosts MUST NOT advertise `multipleChatProviders` on connections that + negotiated a protocol version earlier than `1.1.0`. Clients and hosts that do + not use the capability are unaffected: every chat uses the session's agent. + ## Relationship to the session channel - A chat's [`ChatSummary`](/reference/chat#chatsummary) appears in the session's [`SessionState.chats`](/reference/session#sessionstate) catalog. The session reducer keeps that catalog in sync with the underlying chat lifecycle. @@ -114,6 +152,8 @@ omits the property. Every other property is changed on the session with [`createChat`](/reference/chat#createchat) is a JSON-RPC request. Callers identify the owning session via the request's `channel` parameter (`ahp-session:/`) and MAY supply: - a `title` containing the client-selected initial display title for the target chat, +- a `provider` selecting the agent that backs the chat, when the host + advertises `multipleChatProviders` (see [Chat providers](#chat-providers)), - an `initialMessage` to start the first turn immediately — carrying its own [`model`](/reference/chat#message) / [`agent`](/reference/chat#message) selection — and - a `source` of type [`ChatSource`](/reference/chat#chatsource), either `{ kind: "fork", chat, turnId }` or `{ kind: "sideChat", chat, turnId }`, @@ -235,6 +275,14 @@ durable session catalogs. The protocol does not require provider equality, prohibit active turns universally, or otherwise expose host-specific storage and runtime constraints. +When the host advertises `multipleChatProviders`, every moved chat keeps its +agent, and a moved chat whose agent differs from the destination session's +`provider` MUST carry `provider` in its destination catalog entry. A +`newSession` destination's `provider` is the requested chat's agent. A host +that does not advertise the capability cannot report a per-chat agent, so it +MAY reject, as an unsupported destination, a move into a session with a +different `provider`. See [Chat providers](#chat-providers). + #### Atomic synchronization The host validates and persists ownership, its internal hierarchy, catalog diff --git a/schema/actions.schema.json b/schema/actions.schema.json index ed6a3a8c0..6419e9058 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -240,6 +240,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -3458,7 +3462,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -3504,7 +3508,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -3812,7 +3816,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -3907,6 +3911,10 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat.\n\nGeneric clients use this to omit hidden chats and disable input for\nread-only chats. Absence defaults to {@link ChatInteractivity.Full} for\nbackward compatibility." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + }, "status": { "$ref": "#/$defs/SessionStatus", "description": "Current chat status, matching {@link ChatSummary.status}.\n\nIncludes the activity bits and the orthogonal {@link SessionStatus.IsRead}\nand {@link SessionStatus.IsArchived} flags. Generic clients use these bits\nto present read, unread, or archived chats in session lists without\nsubscribing to the session or chat channel. Absence means the host did\nnot provide the status; clients MUST treat it as unknown, not as unread\nor unarchived." @@ -5220,6 +5228,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -5336,6 +5348,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -5476,7 +5492,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { "values": { "type": "object", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index a30f2b814..adab796dc 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -185,6 +185,10 @@ "tcpConnections": { "$ref": "#/$defs/TcpConnectionsCapability", "description": "Present when the host supports `createTcpConnection`, which opens a TCP\nconnection on the host's network for the client to forward.\n\nStability: 1.0 - Early development." + }, + "multipleChatProviders": { + "$ref": "#/$defs/MultipleChatProvidersCapability", + "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent,\nevery chat uses its session's provider and clients MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -193,6 +197,11 @@ "snapshots" ] }, + "MultipleChatProvidersCapability": { + "type": "object", + "description": "Present when the host supports chats backed by different agents within one\nsession. See {@link InitializeResult.multipleChatProviders}.\n\nThe empty object means \"supported\"; sub-fields are reserved for future\noptions.\n\nStability: 1.0 - Early development.", + "properties": {} + }, "AutomationCapabilities": { "type": "object", "description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.", @@ -1426,6 +1435,10 @@ "title": { "type": "string", "description": "Client-selected initial display title for the new chat." + }, + "provider": { + "type": "string", + "description": "Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat.\nWhen absent, the chat uses the source chat's provider if `source` is\npresent, and otherwise the session's\n{@link SessionMetadata.provider | `provider`}.\n\nA client MUST NOT supply this field unless the host advertises\n{@link InitializeResult.multipleChatProviders}. The host MUST reject the\nrequest with `InvalidParams` when:\n\n- it does not advertise `multipleChatProviders`;\n- the value is not the `provider` of an agent in the root state's\n `agents`, or that agent does not advertise\n {@link AgentCapabilities.multipleChats}; or\n- `source` is present and the value differs from the source chat's\n provider. Forks and side chats always use their source chat's agent;\n history is never transferred between agents.\n\nCapability checks that depend on the agent — `multipleChats.fork`,\n`multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the\nchat's agent. The new chat's\n{@link ChatSummary.provider | `ChatSummary.provider`} reports the\nresolved agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -1470,7 +1483,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -2734,7 +2747,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -2780,7 +2793,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -3088,7 +3101,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -3183,6 +3196,10 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat.\n\nGeneric clients use this to omit hidden chats and disable input for\nread-only chats. Absence defaults to {@link ChatInteractivity.Full} for\nbackward compatibility." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + }, "status": { "$ref": "#/$defs/SessionStatus", "description": "Current chat status, matching {@link ChatSummary.status}.\n\nIncludes the activity bits and the orthogonal {@link SessionStatus.IsRead}\nand {@link SessionStatus.IsArchived} flags. Generic clients use these bits\nto present read, unread, or archived chats in session lists without\nsubscribing to the session or chat channel. Absence means the host did\nnot provide the status; clients MUST treat it as unknown, not as unread\nor unarchived." @@ -4496,6 +4513,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -4612,6 +4633,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -4752,7 +4777,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { "values": { "type": "object", @@ -8188,6 +8213,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 4d466da55..112be7fd6 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -872,7 +872,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -918,7 +918,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1226,7 +1226,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1321,6 +1321,10 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat.\n\nGeneric clients use this to omit hidden chats and disable input for\nread-only chats. Absence defaults to {@link ChatInteractivity.Full} for\nbackward compatibility." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + }, "status": { "$ref": "#/$defs/SessionStatus", "description": "Current chat status, matching {@link ChatSummary.status}.\n\nIncludes the activity bits and the orthogonal {@link SessionStatus.IsRead}\nand {@link SessionStatus.IsArchived} flags. Generic clients use these bits\nto present read, unread, or archived chats in session lists without\nsubscribing to the session or chat channel. Absence means the host did\nnot provide the status; clients MUST treat it as unknown, not as unread\nor unarchived." @@ -2634,6 +2638,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -2750,6 +2758,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -2890,7 +2902,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { "values": { "type": "object", @@ -6271,6 +6283,10 @@ "tcpConnections": { "$ref": "#/$defs/TcpConnectionsCapability", "description": "Present when the host supports `createTcpConnection`, which opens a TCP\nconnection on the host's network for the client to forward.\n\nStability: 1.0 - Early development." + }, + "multipleChatProviders": { + "$ref": "#/$defs/MultipleChatProvidersCapability", + "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent,\nevery chat uses its session's provider and clients MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -6279,6 +6295,11 @@ "snapshots" ] }, + "MultipleChatProvidersCapability": { + "type": "object", + "description": "Present when the host supports chats backed by different agents within one\nsession. See {@link InitializeResult.multipleChatProviders}.\n\nThe empty object means \"supported\"; sub-fields are reserved for future\noptions.\n\nStability: 1.0 - Early development.", + "properties": {} + }, "AutomationCapabilities": { "type": "object", "description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.", @@ -7512,6 +7533,10 @@ "title": { "type": "string", "description": "Client-selected initial display title for the new chat." + }, + "provider": { + "type": "string", + "description": "Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat.\nWhen absent, the chat uses the source chat's provider if `source` is\npresent, and otherwise the session's\n{@link SessionMetadata.provider | `provider`}.\n\nA client MUST NOT supply this field unless the host advertises\n{@link InitializeResult.multipleChatProviders}. The host MUST reject the\nrequest with `InvalidParams` when:\n\n- it does not advertise `multipleChatProviders`;\n- the value is not the `provider` of an agent in the root state's\n `agents`, or that agent does not advertise\n {@link AgentCapabilities.multipleChats}; or\n- `source` is present and the value differs from the source chat's\n provider. Forks and side chats always use their source chat's agent;\n history is never transferred between agents.\n\nCapability checks that depend on the agent — `multipleChats.fork`,\n`multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the\nchat's agent. The new chat's\n{@link ChatSummary.provider | `ChatSummary.provider`} reports the\nresolved agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -7556,7 +7581,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -9378,6 +9403,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 87509d806..7e3c92042 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -141,7 +141,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1174,7 +1174,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1220,7 +1220,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1528,7 +1528,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1623,6 +1623,10 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat.\n\nGeneric clients use this to omit hidden chats and disable input for\nread-only chats. Absence defaults to {@link ChatInteractivity.Full} for\nbackward compatibility." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + }, "status": { "$ref": "#/$defs/SessionStatus", "description": "Current chat status, matching {@link ChatSummary.status}.\n\nIncludes the activity bits and the orthogonal {@link SessionStatus.IsRead}\nand {@link SessionStatus.IsArchived} flags. Generic clients use these bits\nto present read, unread, or archived chats in session lists without\nsubscribing to the session or chat channel. Absence means the host did\nnot provide the status; clients MUST treat it as unknown, not as unread\nor unarchived." @@ -2936,6 +2940,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -3052,6 +3060,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -3192,7 +3204,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { "values": { "type": "object", diff --git a/schema/state.schema.json b/schema/state.schema.json index beb1c4e31..b6a4108d6 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -783,7 +783,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -829,7 +829,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1137,7 +1137,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID" + "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." }, "title": { "type": "string", @@ -1232,6 +1232,10 @@ "$ref": "#/$defs/ChatInteractivity", "description": "How the user can interact with this chat.\n\nGeneric clients use this to omit hidden chats and disable input for\nread-only chats. Absence defaults to {@link ChatInteractivity.Full} for\nbackward compatibility." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + }, "status": { "$ref": "#/$defs/SessionStatus", "description": "Current chat status, matching {@link ChatSummary.status}.\n\nIncludes the activity bits and the orthogonal {@link SessionStatus.IsRead}\nand {@link SessionStatus.IsArchived} flags. Generic clients use these bits\nto present read, unread, or archived chats in session lists without\nsubscribing to the session or chat channel. Absence means the host did\nnot provide the status; clients MUST treat it as unknown, not as unread\nor unarchived." @@ -2545,6 +2549,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -2661,6 +2669,10 @@ "$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." }, + "provider": { + "type": "string", + "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -2801,7 +2813,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { "values": { "type": "object", diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index 8d9717cdb..832ea6722 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -2312,6 +2312,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, { name: 'AutomationCustomizationsCapability' }, + { name: 'MultipleChatProvidersCapability' }, { name: 'ReconnectParams' }, { name: 'ChannelDeliveryResumeParams' }, { name: 'ChannelDeliveryResumeOptions' }, { name: 'ChannelDeliveryResumeResult' }, { name: 'SubscriptionFlowControlOptions' }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index caaee7a4f..5e613e6a7 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1804,6 +1804,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, { name: 'AutomationCustomizationsCapability' }, + { name: 'MultipleChatProvidersCapability' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ChannelDeliveryResumeParams' }, { name: 'ChannelDeliveryResumeOptions' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 0d30f0765..609a705d4 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1788,6 +1788,7 @@ const COMMAND_STRUCTS = [ 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', 'AutomationCustomizationsCapability', + 'MultipleChatProvidersCapability', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'ChannelDeliveryResumeParams', 'ChannelDeliveryResumeOptions', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index f12fedac7..534414f34 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1830,6 +1830,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, { name: 'AutomationCustomizationsCapability' }, + { name: 'MultipleChatProvidersCapability' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ChannelDeliveryResumeParams' }, { name: 'ChannelDeliveryResumeOptions' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 7b8eeb9df..baa2bfec9 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1710,6 +1710,7 @@ const COMMAND_STRUCTS = [ 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', 'AutomationCustomizationsCapability', + 'MultipleChatProvidersCapability', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'ChannelDeliveryResumeParams', 'ChannelDeliveryResumeOptions', diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 381cd2ae7..7bc4f6fee 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -120,6 +120,32 @@ export interface CreateChatParams extends BaseParams { workingDirectories?: URI[]; /** Client-selected initial display title for the new chat. */ title?: string; + /** + * Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. + * When absent, the chat uses the source chat's provider if `source` is + * present, and otherwise the session's + * {@link SessionMetadata.provider | `provider`}. + * + * A client MUST NOT supply this field unless the host advertises + * {@link InitializeResult.multipleChatProviders}. The host MUST reject the + * request with `InvalidParams` when: + * + * - it does not advertise `multipleChatProviders`; + * - the value is not the `provider` of an agent in the root state's + * `agents`, or that agent does not advertise + * {@link AgentCapabilities.multipleChats}; or + * - `source` is present and the value differs from the source chat's + * provider. Forks and side chats always use their source chat's agent; + * history is never transferred between agents. + * + * Capability checks that depend on the agent — `multipleChats.fork`, + * `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the + * chat's agent. The new chat's + * {@link ChatSummary.provider | `ChatSummary.provider`} reports the + * resolved agent. + * @stability 1.0 + */ + provider?: string; } // ─── moveChat ──────────────────────────────────────────────────────────────── @@ -189,6 +215,16 @@ export type ChatMoveDestination = * Rejection leaves ownership, catalog order, chat state, and root summaries * unchanged. * + * When the host advertises {@link InitializeResult.multipleChatProviders}, + * every moved chat keeps its agent ({@link ChatSummary.provider}), and every + * moved chat whose provider differs from the destination session's + * {@link SessionMetadata.provider} MUST carry `provider` in its destination + * catalog entry. A `newSession` destination's session `provider` is the + * requested chat's provider. A host that does not advertise the capability + * cannot report a per-chat provider, so it MAY reject, as an unsupported + * destination, a move that would place a chat in a session with a different + * provider. + * * On success every moved chat keeps its URI, state, and immutable * `ChatOrigin`. The host commits ownership and catalog order before publishing * `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 4455a1cb9..5aea8cc1b 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -82,6 +82,20 @@ export interface ChatState { * compatibility. */ interactivity?: ChatInteractivity; + /** + * Agent provider ID (an {@link AgentInfo.provider}) backing this chat. + * + * Absence means the owning session's + * {@link SessionMetadata.provider | `provider`}; clients MUST treat an + * absent value that way. Hosts MAY omit it when it equals the session's + * provider and MUST set it when it differs, which only happens when the + * host advertises {@link InitializeResult.multipleChatProviders}. A chat's + * agent never changes; hosts MUST NOT change this value through + * `session/chatUpdated`. Forks and side chats always use their source + * chat's agent. + * @stability 1.0 + */ + provider?: string; /** * The subset of the session's * {@link SessionState.workingDirectories | `workingDirectories`} that this @@ -223,6 +237,13 @@ export interface ChatSummary { * compatibility. */ interactivity?: ChatInteractivity; + /** + * Agent provider ID backing this chat. Absence means the owning session's + * {@link SessionMetadata.provider | `provider`}. + * See {@link ChatState.provider} for the full semantics. + * @stability 1.0 + */ + provider?: string; /** * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. @@ -425,6 +446,13 @@ export const enum ChatInteractivity { * these values applied over them. Only properties with `chatMutable: true` * in the session's config schema can be set on a chat. * + * A chat whose {@link ChatState.provider | `provider`} differs from the + * session's provider does not inherit the session's values: the session's + * agent owns those. The host seeds that chat's values from its agent's + * defaults when it creates the chat, and they are the chat's effective + * config. The chat's settings are its own and change only through + * `chat/configChanged` on that chat. + * * @category Chat Config Types */ export interface ChatConfigState { diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 9c36015ca..df512c6ce 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -116,7 +116,11 @@ export type SessionOrigin = AutomationSessionOrigin; * @category Session State */ export interface SessionMetadata { - /** Agent provider ID */ + /** + * Agent provider ID of the session: the agent that backs the session's + * initial chat and every chat that does not report its own + * {@link ChatSummary.provider | `provider`}. + */ provider: string; /** Session title */ title: string; @@ -543,6 +547,15 @@ export interface SessionChatSummary { * backward compatibility. */ interactivity?: ChatInteractivity; + /** + * Agent provider ID backing this chat, matching {@link ChatSummary.provider}. + * + * Absence means the session's {@link SessionMetadata.provider | `provider`}. + * Hosts MUST set it when it differs, so session lists can show sessions + * whose chats use different agents without subscribing to them. + * @stability 1.0 + */ + provider?: string; /** * Current chat status, matching {@link ChatSummary.status}. * diff --git a/types/common/commands.ts b/types/common/commands.ts index e8e599683..2210961da 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -313,8 +313,36 @@ export interface InitializeResult { * @see {@link /specification/tcp-channel | TCP Channel} */ tcpConnections?: TcpConnectionsCapability; + /** + * Present when the host lets chats in one session be backed by different + * agents. Clients MAY then pass {@link CreateChatParams.provider} to + * `createChat`, and chats MAY report a {@link ChatSummary.provider} that + * differs from the session's {@link SessionMetadata.provider}. When absent, + * every chat uses its session's provider and clients MUST NOT send + * {@link CreateChatParams.provider}. + * + * This is a host capability rather than an {@link AgentCapabilities | agent + * capability} because the host, not an individual agent, decides which + * agents may share a session. Hosts MUST NOT advertise it on connections + * that negotiated a protocol version earlier than `1.1.0`. + * @stability 1.0 + * @see {@link /specification/chat-channel#chat-providers | Chat Channel: Chat providers} + */ + multipleChatProviders?: MultipleChatProvidersCapability; } +/** + * Present when the host supports chats backed by different agents within one + * session. See {@link InitializeResult.multipleChatProviders}. + * + * The empty object means "supported"; sub-fields are reserved for future + * options. + * + * @category Commands + * @stability 1.0 + */ +export interface MultipleChatProvidersCapability {} + /** * Automation features supported by this host authority. * diff --git a/types/test-cases/round-trips/076-create-chat-provider.json b/types/test-cases/round-trips/076-create-chat-provider.json new file mode 100644 index 000000000..f92b21975 --- /dev/null +++ b/types/test-cases/round-trips/076-create-chat-provider.json @@ -0,0 +1,18 @@ +{ + "name": "create-chat-provider", + "group": "A", + "description": "CreateChatParams preserves the requested agent provider for a new peer chat.", + "type": "CreateChatParams", + "input": { + "channel": "ahp-session:/s1", + "chat": "ahp-chat:/c2", + "provider": "other-agent" + }, + "acceptableOutputs": [ + { + "channel": "ahp-session:/s1", + "chat": "ahp-chat:/c2", + "provider": "other-agent" + } + ] +} diff --git a/types/test-cases/round-trips/077-initialize-multiple-chat-providers.json b/types/test-cases/round-trips/077-initialize-multiple-chat-providers.json new file mode 100644 index 000000000..a133fc678 --- /dev/null +++ b/types/test-cases/round-trips/077-initialize-multiple-chat-providers.json @@ -0,0 +1,20 @@ +{ + "name": "initialize-multiple-chat-providers", + "group": "A", + "description": "InitializeResult advertises host support for chats backed by different agents through multipleChatProviders.", + "type": "InitializeResult", + "input": { + "protocolVersion": "1.1.0", + "serverSeq": 1, + "snapshots": [], + "multipleChatProviders": {} + }, + "acceptableOutputs": [ + { + "protocolVersion": "1.1.0", + "serverSeq": 1, + "snapshots": [], + "multipleChatProviders": {} + } + ] +} diff --git a/types/test-cases/round-trips/078-chat-summary-provider.json b/types/test-cases/round-trips/078-chat-summary-provider.json new file mode 100644 index 000000000..e08f03b6d --- /dev/null +++ b/types/test-cases/round-trips/078-chat-summary-provider.json @@ -0,0 +1,28 @@ +{ + "name": "chat-summary-provider", + "group": "A", + "description": "A session/chatAdded action preserves a chat's agent provider when it differs from the session's provider.", + "type": "StateAction", + "input": { + "type": "session/chatAdded", + "summary": { + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent" + } + }, + "acceptableOutputs": [ + { + "type": "session/chatAdded", + "summary": { + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent" + } + } + ] +} From 05b755341928bd2c1098ba2c8fb0dd95ed752318 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Oct 2026 15:00:34 +0200 Subject: [PATCH 02/13] chat: optional per-chat config schema for chats on a different agent `ChatConfigState.schema?: SessionConfigSchema` lets a chat carry its own config schema, mirroring the session. Additive; changes still flow through `chat/configChanged`. - Hosts MUST set `schema` (the chat agent's chat-applicable config schema) when a chat's provider differs from the session's, with `values` seeded from that agent's defaults; they MAY omit it otherwise, and clients then use `SessionState.config.schema`. - Clients MUST render and validate a chat's settings against its schema whenever present. - Kotlin reducer: keep `config.schema` on `chat/configChanged` (it rebuilt ChatConfigState from values only). Shared reducer fixture 308 covers it. - Round-trip fixture 079; docs, state-model, and change fragment updated. - read-stability test: expect the three `@stability` markers the previous commit added to generated command files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/State.generated.cs | 32 +++++++--- clients/go/ahptypes/state.generated.go | 30 ++++++--- .../microsoft/agenthostprotocol/Reducers.kt | 2 +- .../generated/State.generated.kt | 14 ++++ clients/rust/crates/ahp-types/src/state.rs | 31 ++++++--- .../Generated/State.generated.swift | 14 ++++ .../.changes/20261019-chat-config-schema.json | 4 ++ docs/guide/state-model.md | 9 ++- docs/specification/chat-channel.md | 21 ++++-- 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 +- scripts/read-stability.test.ts | 2 +- types/channels-chat/state.ts | 32 +++++++--- ...t-configchanged-preserves-chat-schema.json | 60 +++++++++++++++++ .../round-trips/056-chat-config-snapshot.json | 2 +- .../079-chat-config-schema-snapshot.json | 64 +++++++++++++++++++ 19 files changed, 298 insertions(+), 49 deletions(-) create mode 100644 docs/.changes/20261019-chat-config-schema.json create mode 100644 types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json create mode 100644 types/test-cases/round-trips/079-chat-config-schema-snapshot.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 3fd4e2e06..91ae0ee76 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -3537,19 +3537,35 @@ public sealed class SessionConfigState /// Configuration values set on a chat: an overlay on the owning session's /// {@link SessionState.config | `SessionState.config`}. /// -/// It carries only the chat's own values; the session's config schema -/// applies. The effective config of a chat is the session's values with -/// these values applied over them. Only properties with `chatMutable: true` -/// in the session's config schema can be set on a chat. +/// It carries only the chat's own values. When {@link schema} is absent, the +/// session's config schema applies: the effective config of a chat is the +/// session's values with these values applied over them, and only properties +/// with `chatMutable: true` in the session's config schema can be set on a +/// chat. /// /// A chat whose {@link ChatState.provider | `provider`} differs from the -/// session's provider does not inherit the session's values: the session's -/// agent owns those. The host seeds that chat's values from its agent's -/// defaults when it creates the chat, and they are the chat's effective -/// config. The chat's settings are its own and change only through +/// session's provider does not inherit the session's schema or values: the +/// session's agent owns those. The host MUST set {@link schema} to that chat's +/// agent's chat-applicable config schema and seed {@link values} from that +/// agent's defaults when it creates the chat; those values are the chat's +/// effective config. The chat's settings are its own and change only through /// `chat/configChanged` on that chat. public sealed class ChatConfigState { + /// JSON Schema describing the config properties that apply to this chat. + /// + /// The host MUST set it when the chat's provider differs from the session's + /// provider and MAY omit it otherwise. When absent, clients MUST use the + /// session's {@link SessionState.config | `SessionState.config.schema`}. + /// When present, clients MUST render and validate this chat's settings + /// against it instead, and only its properties with `chatMutable: true` + /// can be set with `chat/configChanged`. `chat/configChanged` never changes + /// the schema. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionConfigSchema? Schema { get; set; } + /// Config values set on this chat, keyed by property id public required Dictionary Values { get; set; } } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 05ca7238b..98d4a0fe7 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1726,18 +1726,32 @@ type SessionConfigState struct { // Configuration values set on a chat: an overlay on the owning session's // {@link SessionState.config | `SessionState.config`}. // -// It carries only the chat's own values; the session's config schema -// applies. The effective config of a chat is the session's values with -// these values applied over them. Only properties with `chatMutable: true` -// in the session's config schema can be set on a chat. +// It carries only the chat's own values. When {@link schema} is absent, the +// session's config schema applies: the effective config of a chat is the +// session's values with these values applied over them, and only properties +// with `chatMutable: true` in the session's config schema can be set on a +// chat. // // A chat whose {@link ChatState.provider | `provider`} differs from the -// session's provider does not inherit the session's values: the session's -// agent owns those. The host seeds that chat's values from its agent's -// defaults when it creates the chat, and they are the chat's effective -// config. The chat's settings are its own and change only through +// session's provider does not inherit the session's schema or values: the +// session's agent owns those. The host MUST set {@link schema} to that chat's +// agent's chat-applicable config schema and seed {@link values} from that +// agent's defaults when it creates the chat; those values are the chat's +// effective config. The chat's settings are its own and change only through // `chat/configChanged` on that chat. type ChatConfigState struct { + // JSON Schema describing the config properties that apply to this chat. + // + // The host MUST set it when the chat's provider differs from the session's + // provider and MAY omit it otherwise. When absent, clients MUST use the + // session's {@link SessionState.config | `SessionState.config.schema`}. + // When present, clients MUST render and validate this chat's settings + // against it instead, and only its properties with `chatMutable: true` + // can be set with `chat/configChanged`. `chat/configChanged` never changes + // the schema. + // + // Stability: 1.0 - Early development. + Schema *SessionConfigSchema `json:"schema,omitempty"` // Config values set on this chat, keyed by property id Values map[string]json.RawMessage `json:"values"` } 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 6aaa44802..04f6335b1 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -1634,7 +1634,7 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when val a = action.value val current = state.config val newValues = if (a.replace == true || current == null) a.config else current.values + a.config - state.copy(config = ChatConfigState(values = newValues)) + state.copy(config = current?.copy(values = newValues) ?: ChatConfigState(values = newValues)) } else -> state 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 103539837..230512530 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 @@ -2584,6 +2584,20 @@ data class SessionConfigState( @Serializable data class ChatConfigState( + /** + * JSON Schema describing the config properties that apply to this chat. + * + * The host MUST set it when the chat's provider differs from the session's + * provider and MAY omit it otherwise. When absent, clients MUST use the + * session's {@link SessionState.config | `SessionState.config.schema`}. + * When present, clients MUST render and validate this chat's settings + * against it instead, and only its properties with `chatMutable: true` + * can be set with `chat/configChanged`. `chat/configChanged` never changes + * the schema. + * + * Stability: 1.0 - Early development. + */ + val schema: SessionConfigSchema? = null, /** * Config values set on this chat, keyed by property id */ diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 3537011c0..607d6f23c 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2909,20 +2909,35 @@ pub struct SessionConfigState { /// Configuration values set on a chat: an overlay on the owning session's /// {@link SessionState.config | `SessionState.config`}. /// -/// It carries only the chat's own values; the session's config schema -/// applies. The effective config of a chat is the session's values with -/// these values applied over them. Only properties with `chatMutable: true` -/// in the session's config schema can be set on a chat. +/// It carries only the chat's own values. When {@link schema} is absent, the +/// session's config schema applies: the effective config of a chat is the +/// session's values with these values applied over them, and only properties +/// with `chatMutable: true` in the session's config schema can be set on a +/// chat. /// /// A chat whose {@link ChatState.provider | `provider`} differs from the -/// session's provider does not inherit the session's values: the session's -/// agent owns those. The host seeds that chat's values from its agent's -/// defaults when it creates the chat, and they are the chat's effective -/// config. The chat's settings are its own and change only through +/// session's provider does not inherit the session's schema or values: the +/// session's agent owns those. The host MUST set {@link schema} to that chat's +/// agent's chat-applicable config schema and seed {@link values} from that +/// agent's defaults when it creates the chat; those values are the chat's +/// effective config. The chat's settings are its own and change only through /// `chat/configChanged` on that chat. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { + /// JSON Schema describing the config properties that apply to this chat. + /// + /// The host MUST set it when the chat's provider differs from the session's + /// provider and MAY omit it otherwise. When absent, clients MUST use the + /// session's {@link SessionState.config | `SessionState.config.schema`}. + /// When present, clients MUST render and validate this chat's settings + /// against it instead, and only its properties with `chatMutable: true` + /// can be set with `chat/configChanged`. `chat/configChanged` never changes + /// the schema. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schema: Option, /// Config values set on this chat, keyed by property id pub values: JsonObject, } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 2926fb4ea..0dc4ad373 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -2708,12 +2708,26 @@ public struct SessionConfigState: Codable, Sendable { } public struct ChatConfigState: Codable, Sendable { + /// JSON Schema describing the config properties that apply to this chat. + /// + /// The host MUST set it when the chat's provider differs from the session's + /// provider and MAY omit it otherwise. When absent, clients MUST use the + /// session's {@link SessionState.config | `SessionState.config.schema`}. + /// When present, clients MUST render and validate this chat's settings + /// against it instead, and only its properties with `chatMutable: true` + /// can be set with `chat/configChanged`. `chat/configChanged` never changes + /// the schema. + /// + /// Stability: 1.0 - Early development. + public var schema: SessionConfigSchema? /// Config values set on this chat, keyed by property id public var values: [String: AnyCodable] public init( + schema: SessionConfigSchema? = nil, values: [String: AnyCodable] ) { + self.schema = schema self.values = values } } diff --git a/docs/.changes/20261019-chat-config-schema.json b/docs/.changes/20261019-chat-config-schema.json new file mode 100644 index 000000000..829ca6b17 --- /dev/null +++ b/docs/.changes/20261019-chat-config-schema.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Optional `ChatConfigState.schema` lets a chat carry its own config schema. Hosts set it for chats backed by a different agent than their session, whose values start from that agent's defaults; when absent, clients use the session's config schema." +} diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index dcb5efa56..d362a8694 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -214,7 +214,8 @@ working directories: the chat's subset when present, otherwise the full session set. `config` is an overlay on `SessionState.config`: it carries only the values set -on this chat, and the session's config schema applies. A chat's effective +on this chat, and the session's config schema applies unless the chat carries +its own `config.schema`. A chat's effective config is the session's values with the chat's values applied over them. Only properties marked `chatMutable: true` in the session's schema can be set per chat, with `chat/configChanged`. See @@ -224,8 +225,10 @@ chat, with `chat/configChanged`. See Hosts that advertise `multipleChatProviders` in their `initialize` result may back chats in one session with different agents, chosen by `createChat`'s `provider`; forks and side chats keep their source chat's agent. A chat whose -agent differs from the session's starts from that agent's config defaults -rather than inheriting the session's values. See +agent differs from the session's carries that agent's config schema in +`config.schema` and starts from that agent's default values rather than +inheriting the session's; clients render and validate its settings against +that schema. See [Chat providers](/specification/chat-channel#chat-providers). Fork and side-chat creation both reference source turns by stable identifiers. diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 339afd245..9f60ad718 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -80,7 +80,8 @@ fully replaces the catalogue or clears it when `changesets` is absent. [`ChatState.config`](/reference/chat#chatstate) is an overlay on the owning session's [`SessionState.config`](/reference/session#sessionstate). It carries -only the values set on this chat; the session's config schema applies. A +only the values set on this chat and, optionally, a `schema`. When `schema` is +absent, the session's config schema applies. A chat's effective config is the session's values with the chat's values applied over them. Hosts do not mirror session config into chats. @@ -94,11 +95,16 @@ omits the property. Every other property is changed on the session with `session/configChanged`. A chat backed by a different agent than its session (see -[Chat providers](#chat-providers)) does not inherit the session's values, -which belong to the session's agent. The host seeds that chat's -`ChatState.config` from its own agent's defaults when it creates the chat, and -those values are the chat's effective config. The chat's settings are its own -and change only through `chat/configChanged` on that chat. +[Chat providers](#chat-providers)) does not inherit the session's schema or +values, which belong to the session's agent. The host MUST set +`ChatState.config.schema` to that agent's chat-applicable config schema and +seed `ChatState.config.values` from that agent's defaults when it creates the +chat; those values are the chat's effective config. When a chat uses the +session's agent, the host MAY omit `schema`, and clients MUST then use the +session's `SessionState.config.schema`. Whenever `schema` is present, clients +MUST render and validate the chat's settings against it rather than the +session's schema. The chat's settings change only through `chat/configChanged` +on that chat, which never changes the schema. ### Chat providers @@ -127,6 +133,9 @@ chat. A host that advertises agents. - Agent capability checks (`multipleChats.fork`, `multipleChats.sideChat`, `multipleWorkingDirectories`) use the chat's agent. +- A chat whose agent differs from the session's carries its own config schema + and agent-default values in `ChatState.config`; see + [Per-chat configuration](#per-chat-configuration). - Hosts MUST NOT advertise `multipleChatProviders` on connections that negotiated a protocol version earlier than `1.1.0`. Clients and hosts that do not use the capability are unaffected: every chat uses the session's agent. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 6419e9058..48ad267e5 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5492,8 +5492,12 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index adab796dc..fa32405e2 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4777,8 +4777,12 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 112be7fd6..b9c9bb69a 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2902,8 +2902,12 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 7e3c92042..51c13bc70 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3204,8 +3204,12 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, diff --git a/schema/state.schema.json b/schema/state.schema.json index b6a4108d6..5a8922a02 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2813,8 +2813,12 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's values: the session's\nagent owns those. The host seeds that chat's values from its agent's\ndefaults when it creates the chat, and they are the chat's effective\nconfig. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, diff --git a/scripts/read-stability.test.ts b/scripts/read-stability.test.ts index 4770e0638..92985e568 100644 --- a/scripts/read-stability.test.ts +++ b/scripts/read-stability.test.ts @@ -113,7 +113,7 @@ test('native client docs preserve declaration and entrypoint stability', () => { ['clients/dotnet/src/AgentHostProtocol.Abstractions/Generated', 'Commands.generated.cs', 'Notifications.generated.cs'], ]; for (const [directory, commands, notifications] of clients) { - for (const [file, expected] of [[commands, 11], [notifications, 6]] as const) { + for (const [file, expected] of [[commands, 14], [notifications, 6]] as const) { const content = readFileSync(resolve(root, directory, file), 'utf8'); assert.equal(content.split(marker).length - 1, expected, `${directory}/${file}`); } diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 5aea8cc1b..4aa80bfdb 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -12,6 +12,7 @@ import type { AgentSelection, ChangesSummary, McpAuthRequirement, + SessionConfigSchema, SessionStatus, } from '../channels-session/state.js'; import type { Changeset } from '../channels-changeset/state.js'; @@ -441,21 +442,36 @@ export const enum ChatInteractivity { * Configuration values set on a chat: an overlay on the owning session's * {@link SessionState.config | `SessionState.config`}. * - * It carries only the chat's own values; the session's config schema - * applies. The effective config of a chat is the session's values with - * these values applied over them. Only properties with `chatMutable: true` - * in the session's config schema can be set on a chat. + * It carries only the chat's own values. When {@link schema} is absent, the + * session's config schema applies: the effective config of a chat is the + * session's values with these values applied over them, and only properties + * with `chatMutable: true` in the session's config schema can be set on a + * chat. * * A chat whose {@link ChatState.provider | `provider`} differs from the - * session's provider does not inherit the session's values: the session's - * agent owns those. The host seeds that chat's values from its agent's - * defaults when it creates the chat, and they are the chat's effective - * config. The chat's settings are its own and change only through + * session's provider does not inherit the session's schema or values: the + * session's agent owns those. The host MUST set {@link schema} to that chat's + * agent's chat-applicable config schema and seed {@link values} from that + * agent's defaults when it creates the chat; those values are the chat's + * effective config. The chat's settings are its own and change only through * `chat/configChanged` on that chat. * * @category Chat Config Types */ export interface ChatConfigState { + /** + * JSON Schema describing the config properties that apply to this chat. + * + * The host MUST set it when the chat's provider differs from the session's + * provider and MAY omit it otherwise. When absent, clients MUST use the + * session's {@link SessionState.config | `SessionState.config.schema`}. + * When present, clients MUST render and validate this chat's settings + * against it instead, and only its properties with `chatMutable: true` + * can be set with `chat/configChanged`. `chat/configChanged` never changes + * the schema. + * @stability 1.0 + */ + schema?: SessionConfigSchema; /** Config values set on this chat, keyed by property id */ values: Record; } diff --git a/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json b/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json new file mode 100644 index 000000000..32636f445 --- /dev/null +++ b/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json @@ -0,0 +1,60 @@ +{ + "description": "chat/configChanged merges values and preserves the chat's config schema", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": false + } + } + }, + "actions": [ + { + "type": "chat/configChanged", + "config": { + "autoApprove": true + } + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": true + } + } + } +} diff --git a/types/test-cases/round-trips/056-chat-config-snapshot.json b/types/test-cases/round-trips/056-chat-config-snapshot.json index c22144f61..725520488 100644 --- a/types/test-cases/round-trips/056-chat-config-snapshot.json +++ b/types/test-cases/round-trips/056-chat-config-snapshot.json @@ -1,7 +1,7 @@ { "name": "chat-config-snapshot", "group": "A", - "description": "ChatState.config is an overlay that carries only the chat's own config values (no schema); it survives a decode/re-encode cycle on a chat snapshot.", + "description": "ChatState.config is an overlay that carries only the chat's own config values when it has no schema; it survives a decode/re-encode cycle on a chat snapshot.", "type": "Snapshot", "input": { "resource": "ahp-chat:/peer", diff --git a/types/test-cases/round-trips/079-chat-config-schema-snapshot.json b/types/test-cases/round-trips/079-chat-config-schema-snapshot.json new file mode 100644 index 000000000..c0cb4118e --- /dev/null +++ b/types/test-cases/round-trips/079-chat-config-schema-snapshot.json @@ -0,0 +1,64 @@ +{ + "name": "chat-config-schema-snapshot", + "group": "A", + "description": "A chat snapshot preserves a per-chat config schema for a chat backed by a different agent than its session.", + "type": "Snapshot", + "input": { + "resource": "ahp-chat:/c2", + "state": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": false + } + } + }, + "fromSeq": 4 + }, + "acceptableOutputs": [ + { + "resource": "ahp-chat:/c2", + "state": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": false + } + } + }, + "fromSeq": 4 + } + ] +} From 66ed59144cd40f7d3c27a217487d44fd68712907 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Oct 2026 15:26:08 +0200 Subject: [PATCH 03/13] chat: tighten per-chat provider and config schema semantics Review follow-ups on the per-chat provider and config schema. - Rust reducer: initialize `ChatConfigState.schema` (compile fix). - `ChatConfigState.schema` is set if and only if the chat's agent differs from the session's. With it, `values` is the full effective config and omitted properties take their schema defaults (including after `replace`); without it, `values` stays an overlay on the session config. - Hosts MUST reject `chat/configChanged` keys invalid for the chat's effective schema. - moveChat: the chat keeps its provider. Hosts with `multipleChatProviders` set `ChatState.provider` on movable chats and normalize `config` for the destination session. Clients resubscribe to observe a changed schema. - `provider` is an identity field in `session/chatUpdated`. The TS session reducer strips it; native clients already merge an explicit field list. - Honest compatibility wording for clients unaware of `provider`. - Reducer fixtures 309 (chatUpdated ignores provider) and 310 (replace preserves schema). Change fragments updated, plus a Kotlin note on positional constructor/componentN changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Actions.generated.cs | 8 +- .../Generated/Commands.generated.cs | 24 ++++- .../Generated/State.generated.cs | 63 +++++++------ clients/go/ahptypes/actions.generated.go | 8 +- clients/go/ahptypes/commands.generated.go | 24 ++++- clients/go/ahptypes/state.generated.go | 63 +++++++------ .../generated/Actions.generated.kt | 4 +- .../generated/State.generated.kt | 29 +++--- clients/rust/crates/ahp-types/src/actions.rs | 8 +- clients/rust/crates/ahp-types/src/commands.rs | 24 ++++- clients/rust/crates/ahp-types/src/state.rs | 63 +++++++------ clients/rust/crates/ahp/src/reducers.rs | 1 + .../Generated/Actions.generated.swift | 4 +- .../Generated/State.generated.swift | 29 +++--- .../.changes/20261019-chat-config-schema.json | 2 +- ...0261019-kotlin-positional-chat-fields.json | 5 ++ docs/guide/state-model.md | 24 ++--- docs/specification/chat-channel.md | 90 ++++++++++++------- schema/actions.schema.json | 14 +-- schema/commands.schema.json | 16 ++-- schema/errors.schema.json | 16 ++-- schema/notifications.schema.json | 10 +-- schema/state.schema.json | 10 +-- types/channels-chat/commands.ts | 24 ++++- types/channels-chat/state.ts | 69 ++++++++------ types/channels-session/actions.ts | 8 +- types/channels-session/reducer.ts | 2 +- ...-session-chatupdated-ignores-provider.json | 46 ++++++++++ ...changed-replace-preserves-chat-schema.json | 57 ++++++++++++ 29 files changed, 511 insertions(+), 234 deletions(-) create mode 100644 docs/.changes/20261019-kotlin-positional-chat-fields.json create mode 100644 types/test-cases/reducers/309-session-chatupdated-ignores-provider.json create mode 100644 types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index 5c81dc828..5c90bedf5 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -1163,8 +1163,8 @@ public sealed record SessionChatRemovedAction /// One existing chat's summary fields changed. /// /// Partial-update semantics: only fields present in `changes` are written; -/// omitted fields are preserved. Identity fields (`resource`) MUST NOT be -/// carried in `changes`. No-op when no entry with `chat` exists — clients +/// omitted fields are preserved. Identity fields (`resource`, `provider`) MUST +/// NOT be carried in `changes`. No-op when no entry with `chat` exists — clients /// SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. /// /// Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -1180,8 +1180,8 @@ public sealed record SessionChatUpdatedAction /// Mutable summary fields that changed; omitted fields are unchanged. /// - /// Identity fields (`resource`) never change and MUST be omitted by - /// senders; receivers SHOULD ignore them if present. + /// Identity fields (`resource` and `provider`) never change and MUST be + /// omitted by senders; receivers SHOULD ignore them if present. public required PartialChatSummary Changes { get; init; } } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index d54257776..5e35b10ca 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -1094,8 +1094,28 @@ public sealed record ChatMoveToNewSessionDestination /// destination, a move that would place a chat in a session with a different /// provider. /// -/// On success every moved chat keeps its URI, state, and immutable -/// `ChatOrigin`. The host commits ownership and catalog order before publishing +/// A moved chat always keeps its `provider`. Because no action changes +/// `ChatState.provider`, a host that advertises the capability MUST set +/// `provider` on the `ChatState` of every chat it advertises as `movable`. +/// When ownership changes, such a host MUST normalize each moved chat's +/// {@link ChatConfigState | `ChatState.config`} for the destination session as +/// part of the committed move: +/// +/// - If the chat's provider differs from the destination session's provider, +/// `config.schema` is the chat agent's chat-applicable config schema and +/// `config.values` is that agent's defaults overlaid with the chat's existing +/// values that are valid in that schema. +/// - If the providers are equal, `config.schema` is removed and +/// `config.values` keeps, as an overlay, only the existing values whose +/// properties the destination session's schema declares `chatMutable`. +/// +/// No action carries a changed `schema`, so clients subscribed to a moved chat +/// whose provider relationship to its session changed SHOULD resubscribe to +/// obtain the normalized `config`. +/// +/// On success every moved chat keeps its URI, its state (apart from the +/// `config` normalization above), and its immutable `ChatOrigin`. The host +/// commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and /// root summary updates as applicable. Session and root snapshots are the /// durable recovery path after reconnect or an uncertain response. diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 91ae0ee76..7eb94db13 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2507,8 +2507,9 @@ public sealed class ChatState /// absent value that way. Hosts MAY omit it when it equals the session's /// provider and MUST set it when it differs, which only happens when the /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's - /// agent never changes; hosts MUST NOT change this value through - /// `session/chatUpdated`. Forks and side chats always use their source + /// agent never changes, including across `moveChat`: like `resource`, it is + /// an identity field that senders MUST omit from `session/chatUpdated` + /// changes and receivers ignore. Forks and side chats always use their source /// chat's agent. /// /// Stability: 1.0 - Early development. @@ -2559,8 +2560,9 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Canvases { get; set; } - /// Configuration values set on this chat, applied over the owning session's - /// {@link SessionState.config | `SessionState.config`}. See + /// Configuration set on this chat: an overlay on the owning session's + /// {@link SessionState.config | `SessionState.config`}, or the chat's own + /// schema and full values when its agent differs from the session's. See /// {@link ChatConfigState}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatConfigState? Config { get; set; } @@ -3534,39 +3536,48 @@ public sealed class SessionConfigState public required Dictionary Values { get; set; } } -/// Configuration values set on a chat: an overlay on the owning session's -/// {@link SessionState.config | `SessionState.config`}. +/// Configuration set on a chat. Its meaning depends on whether {@link schema} +/// is present. /// -/// It carries only the chat's own values. When {@link schema} is absent, the -/// session's config schema applies: the effective config of a chat is the -/// session's values with these values applied over them, and only properties -/// with `chatMutable: true` in the session's config schema can be set on a -/// chat. +/// - **Without `schema`** (the chat uses the session's agent): {@link values} +/// is an overlay on the owning session's +/// {@link SessionState.config | `SessionState.config`}. The session's config +/// schema applies; the chat's effective config is the session's values with +/// these values applied over them, and only properties with +/// `chatMutable: true` in the session's schema can be set on the chat. A +/// property omitted from `values` takes the session's value. +/// - **With `schema`** (the chat's {@link ChatState.provider | `provider`} +/// differs from the session's): {@link values} is the chat's full effective +/// config and nothing is inherited from the session, whose schema and values +/// belong to the session's agent. The host seeds `values` from the chat +/// agent's defaults when it creates the chat. A property omitted from +/// `values`, including after a `chat/configChanged` with `replace: true`, +/// takes its `default` from `schema`. /// -/// A chat whose {@link ChatState.provider | `provider`} differs from the -/// session's provider does not inherit the session's schema or values: the -/// session's agent owns those. The host MUST set {@link schema} to that chat's -/// agent's chat-applicable config schema and seed {@link values} from that -/// agent's defaults when it creates the chat; those values are the chat's -/// effective config. The chat's settings are its own and change only through -/// `chat/configChanged` on that chat. +/// Settings change only through `chat/configChanged` on the chat, which never +/// changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not +/// valid for the chat's effective schema (`schema` when present, otherwise the +/// session's schema restricted to `chatMutable` properties). public sealed class ChatConfigState { /// JSON Schema describing the config properties that apply to this chat. /// - /// The host MUST set it when the chat's provider differs from the session's - /// provider and MAY omit it otherwise. When absent, clients MUST use the - /// session's {@link SessionState.config | `SessionState.config.schema`}. - /// When present, clients MUST render and validate this chat's settings - /// against it instead, and only its properties with `chatMutable: true` - /// can be set with `chat/configChanged`. `chat/configChanged` never changes - /// the schema. + /// The host MUST set it if and only if the chat's + /// {@link ChatState.provider | `provider`} differs from the session's + /// provider, to that agent's chat-applicable config schema, and MUST NOT set + /// it otherwise. When absent, clients MUST use the session's + /// {@link SessionState.config | `SessionState.config.schema`}. When present, + /// clients MUST render and validate this chat's settings against it instead, + /// and only its properties with `chatMutable: true` can be set with + /// `chat/configChanged`. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionConfigSchema? Schema { get; set; } - /// Config values set on this chat, keyed by property id + /// Config values set on this chat, keyed by property id. An overlay on the + /// session's values when {@link schema} is absent; the chat's full effective + /// config when it is present. public required Dictionary Values { get; set; } } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 88a18008c..4d9feebc4 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -210,8 +210,8 @@ type SessionChatRemovedAction struct { // One existing chat's summary fields changed. // // Partial-update semantics: only fields present in `changes` are written; -// omitted fields are preserved. Identity fields (`resource`) MUST NOT be -// carried in `changes`. No-op when no entry with `chat` exists — clients +// omitted fields are preserved. Identity fields (`resource`, `provider`) MUST +// NOT be carried in `changes`. No-op when no entry with `chat` exists — clients // SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. // // Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -224,8 +224,8 @@ type SessionChatUpdatedAction struct { Chat URI `json:"chat"` // Mutable summary fields that changed; omitted fields are unchanged. // - // Identity fields (`resource`) never change and MUST be omitted by - // senders; receivers SHOULD ignore them if present. + // Identity fields (`resource` and `provider`) never change and MUST be + // omitted by senders; receivers SHOULD ignore them if present. Changes PartialChatSummary `json:"changes"` } diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index baa77b8f1..c59e776ca 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -759,8 +759,28 @@ type ChatMoveToNewSessionDestination struct { // destination, a move that would place a chat in a session with a different // provider. // -// On success every moved chat keeps its URI, state, and immutable -// `ChatOrigin`. The host commits ownership and catalog order before publishing +// A moved chat always keeps its `provider`. Because no action changes +// `ChatState.provider`, a host that advertises the capability MUST set +// `provider` on the `ChatState` of every chat it advertises as `movable`. +// When ownership changes, such a host MUST normalize each moved chat's +// {@link ChatConfigState | `ChatState.config`} for the destination session as +// part of the committed move: +// +// - If the chat's provider differs from the destination session's provider, +// `config.schema` is the chat agent's chat-applicable config schema and +// `config.values` is that agent's defaults overlaid with the chat's existing +// values that are valid in that schema. +// - If the providers are equal, `config.schema` is removed and +// `config.values` keeps, as an overlay, only the existing values whose +// properties the destination session's schema declares `chatMutable`. +// +// No action carries a changed `schema`, so clients subscribed to a moved chat +// whose provider relationship to its session changed SHOULD resubscribe to +// obtain the normalized `config`. +// +// On success every moved chat keeps its URI, its state (apart from the +// `config` normalization above), and its immutable `ChatOrigin`. The host +// commits ownership and catalog order before publishing // `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and // root summary updates as applicable. Session and root snapshots are the // durable recovery path after reconnect or an uncertain response. diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 98d4a0fe7..df05b2f36 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1411,8 +1411,9 @@ type ChatState struct { // absent value that way. Hosts MAY omit it when it equals the session's // provider and MUST set it when it differs, which only happens when the // host advertises {@link InitializeResult.multipleChatProviders}. A chat's - // agent never changes; hosts MUST NOT change this value through - // `session/chatUpdated`. Forks and side chats always use their source + // agent never changes, including across `moveChat`: like `resource`, it is + // an identity field that senders MUST omit from `session/chatUpdated` + // changes and receivers ignore. Forks and side chats always use their source // chat's agent. // // Stability: 1.0 - Early development. @@ -1453,8 +1454,9 @@ type ChatState struct { // Clients subscribe to each resource for the experimental presentation // state, including its current live source URL. Canvases []CanvasReference `json:"canvases,omitempty"` - // Configuration values set on this chat, applied over the owning session's - // {@link SessionState.config | `SessionState.config`}. See + // Configuration set on this chat: an overlay on the owning session's + // {@link SessionState.config | `SessionState.config`}, or the chat's own + // schema and full values when its agent differs from the session's. See // {@link ChatConfigState}. Config *ChatConfigState `json:"config,omitempty"` // Completed turns @@ -1723,36 +1725,45 @@ type SessionConfigState struct { Values map[string]json.RawMessage `json:"values"` } -// Configuration values set on a chat: an overlay on the owning session's -// {@link SessionState.config | `SessionState.config`}. +// Configuration set on a chat. Its meaning depends on whether {@link schema} +// is present. // -// It carries only the chat's own values. When {@link schema} is absent, the -// session's config schema applies: the effective config of a chat is the -// session's values with these values applied over them, and only properties -// with `chatMutable: true` in the session's config schema can be set on a -// chat. +// - **Without `schema`** (the chat uses the session's agent): {@link values} +// is an overlay on the owning session's +// {@link SessionState.config | `SessionState.config`}. The session's config +// schema applies; the chat's effective config is the session's values with +// these values applied over them, and only properties with +// `chatMutable: true` in the session's schema can be set on the chat. A +// property omitted from `values` takes the session's value. +// - **With `schema`** (the chat's {@link ChatState.provider | `provider`} +// differs from the session's): {@link values} is the chat's full effective +// config and nothing is inherited from the session, whose schema and values +// belong to the session's agent. The host seeds `values` from the chat +// agent's defaults when it creates the chat. A property omitted from +// `values`, including after a `chat/configChanged` with `replace: true`, +// takes its `default` from `schema`. // -// A chat whose {@link ChatState.provider | `provider`} differs from the -// session's provider does not inherit the session's schema or values: the -// session's agent owns those. The host MUST set {@link schema} to that chat's -// agent's chat-applicable config schema and seed {@link values} from that -// agent's defaults when it creates the chat; those values are the chat's -// effective config. The chat's settings are its own and change only through -// `chat/configChanged` on that chat. +// Settings change only through `chat/configChanged` on the chat, which never +// changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not +// valid for the chat's effective schema (`schema` when present, otherwise the +// session's schema restricted to `chatMutable` properties). type ChatConfigState struct { // JSON Schema describing the config properties that apply to this chat. // - // The host MUST set it when the chat's provider differs from the session's - // provider and MAY omit it otherwise. When absent, clients MUST use the - // session's {@link SessionState.config | `SessionState.config.schema`}. - // When present, clients MUST render and validate this chat's settings - // against it instead, and only its properties with `chatMutable: true` - // can be set with `chat/configChanged`. `chat/configChanged` never changes - // the schema. + // The host MUST set it if and only if the chat's + // {@link ChatState.provider | `provider`} differs from the session's + // provider, to that agent's chat-applicable config schema, and MUST NOT set + // it otherwise. When absent, clients MUST use the session's + // {@link SessionState.config | `SessionState.config.schema`}. When present, + // clients MUST render and validate this chat's settings against it instead, + // and only its properties with `chatMutable: true` can be set with + // `chat/configChanged`. // // Stability: 1.0 - Early development. Schema *SessionConfigSchema `json:"schema,omitempty"` - // Config values set on this chat, keyed by property id + // Config values set on this chat, keyed by property id. An overlay on the + // session's values when {@link schema} is absent; the chat's full effective + // config when it is present. Values map[string]json.RawMessage `json:"values"` } 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 f4124ee0a..365b97e52 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 @@ -231,8 +231,8 @@ data class SessionChatUpdatedAction( /** * Mutable summary fields that changed; omitted fields are unchanged. * - * Identity fields (`resource`) never change and MUST be omitted by - * senders; receivers SHOULD ignore them if present. + * Identity fields (`resource` and `provider`) never change and MUST be + * omitted by senders; receivers SHOULD ignore them if present. */ val changes: PartialChatSummary ) 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 230512530..20af581a0 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 @@ -1819,8 +1819,9 @@ data class ChatState( * absent value that way. Hosts MAY omit it when it equals the session's * provider and MUST set it when it differs, which only happens when the * host advertises {@link InitializeResult.multipleChatProviders}. A chat's - * agent never changes; hosts MUST NOT change this value through - * `session/chatUpdated`. Forks and side chats always use their source + * agent never changes, including across `moveChat`: like `resource`, it is + * an identity field that senders MUST omit from `session/chatUpdated` + * changes and receivers ignore. Forks and side chats always use their source * chat's agent. * * Stability: 1.0 - Early development. @@ -1871,8 +1872,9 @@ data class ChatState( */ val canvases: List? = null, /** - * Configuration values set on this chat, applied over the owning session's - * {@link SessionState.config | `SessionState.config`}. See + * Configuration set on this chat: an overlay on the owning session's + * {@link SessionState.config | `SessionState.config`}, or the chat's own + * schema and full values when its agent differs from the session's. See * {@link ChatConfigState}. */ val config: ChatConfigState? = null, @@ -2587,19 +2589,22 @@ data class ChatConfigState( /** * JSON Schema describing the config properties that apply to this chat. * - * The host MUST set it when the chat's provider differs from the session's - * provider and MAY omit it otherwise. When absent, clients MUST use the - * session's {@link SessionState.config | `SessionState.config.schema`}. - * When present, clients MUST render and validate this chat's settings - * against it instead, and only its properties with `chatMutable: true` - * can be set with `chat/configChanged`. `chat/configChanged` never changes - * the schema. + * The host MUST set it if and only if the chat's + * {@link ChatState.provider | `provider`} differs from the session's + * provider, to that agent's chat-applicable config schema, and MUST NOT set + * it otherwise. When absent, clients MUST use the session's + * {@link SessionState.config | `SessionState.config.schema`}. When present, + * clients MUST render and validate this chat's settings against it instead, + * and only its properties with `chatMutable: true` can be set with + * `chat/configChanged`. * * Stability: 1.0 - Early development. */ val schema: SessionConfigSchema? = null, /** - * Config values set on this chat, keyed by property id + * Config values set on this chat, keyed by property id. An overlay on the + * session's values when {@link schema} is absent; the chat's full effective + * config when it is present. */ val values: Map ) diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 96a41ecf0..f8da218dd 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -543,8 +543,8 @@ pub struct SessionChatRemovedAction { /// One existing chat's summary fields changed. /// /// Partial-update semantics: only fields present in `changes` are written; -/// omitted fields are preserved. Identity fields (`resource`) MUST NOT be -/// carried in `changes`. No-op when no entry with `chat` exists — clients +/// omitted fields are preserved. Identity fields (`resource`, `provider`) MUST +/// NOT be carried in `changes`. No-op when no entry with `chat` exists — clients /// SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. /// /// Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -558,8 +558,8 @@ pub struct SessionChatUpdatedAction { pub chat: Uri, /// Mutable summary fields that changed; omitted fields are unchanged. /// - /// Identity fields (`resource`) never change and MUST be omitted by - /// senders; receivers SHOULD ignore them if present. + /// Identity fields (`resource` and `provider`) never change and MUST be + /// omitted by senders; receivers SHOULD ignore them if present. pub changes: PartialChatSummary, } diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index c8d95118c..875b32a46 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -1019,8 +1019,28 @@ pub struct ChatMoveToNewSessionDestination {} /// destination, a move that would place a chat in a session with a different /// provider. /// -/// On success every moved chat keeps its URI, state, and immutable -/// `ChatOrigin`. The host commits ownership and catalog order before publishing +/// A moved chat always keeps its `provider`. Because no action changes +/// `ChatState.provider`, a host that advertises the capability MUST set +/// `provider` on the `ChatState` of every chat it advertises as `movable`. +/// When ownership changes, such a host MUST normalize each moved chat's +/// {@link ChatConfigState | `ChatState.config`} for the destination session as +/// part of the committed move: +/// +/// - If the chat's provider differs from the destination session's provider, +/// `config.schema` is the chat agent's chat-applicable config schema and +/// `config.values` is that agent's defaults overlaid with the chat's existing +/// values that are valid in that schema. +/// - If the providers are equal, `config.schema` is removed and +/// `config.values` keeps, as an overlay, only the existing values whose +/// properties the destination session's schema declares `chatMutable`. +/// +/// No action carries a changed `schema`, so clients subscribed to a moved chat +/// whose provider relationship to its session changed SHOULD resubscribe to +/// obtain the normalized `config`. +/// +/// On success every moved chat keeps its URI, its state (apart from the +/// `config` normalization above), and its immutable `ChatOrigin`. The host +/// commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and /// root summary updates as applicable. Session and root snapshots are the /// durable recovery path after reconnect or an uncertain response. diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 607d6f23c..62f760dbd 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2070,8 +2070,9 @@ pub struct ChatState { /// absent value that way. Hosts MAY omit it when it equals the session's /// provider and MUST set it when it differs, which only happens when the /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's - /// agent never changes; hosts MUST NOT change this value through - /// `session/chatUpdated`. Forks and side chats always use their source + /// agent never changes, including across `moveChat`: like `resource`, it is + /// an identity field that senders MUST omit from `session/chatUpdated` + /// changes and receivers ignore. Forks and side chats always use their source /// chat's agent. /// /// Stability: 1.0 - Early development. @@ -2117,8 +2118,9 @@ pub struct ChatState { /// state, including its current live source URL. #[serde(default, skip_serializing_if = "Option::is_none")] pub canvases: Option>, - /// Configuration values set on this chat, applied over the owning session's - /// {@link SessionState.config | `SessionState.config`}. See + /// Configuration set on this chat: an overlay on the owning session's + /// {@link SessionState.config | `SessionState.config`}, or the chat's own + /// schema and full values when its agent differs from the session's. See /// {@link ChatConfigState}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, @@ -2906,39 +2908,48 @@ pub struct SessionConfigState { pub values: JsonObject, } -/// Configuration values set on a chat: an overlay on the owning session's -/// {@link SessionState.config | `SessionState.config`}. +/// Configuration set on a chat. Its meaning depends on whether {@link schema} +/// is present. /// -/// It carries only the chat's own values. When {@link schema} is absent, the -/// session's config schema applies: the effective config of a chat is the -/// session's values with these values applied over them, and only properties -/// with `chatMutable: true` in the session's config schema can be set on a -/// chat. +/// - **Without `schema`** (the chat uses the session's agent): {@link values} +/// is an overlay on the owning session's +/// {@link SessionState.config | `SessionState.config`}. The session's config +/// schema applies; the chat's effective config is the session's values with +/// these values applied over them, and only properties with +/// `chatMutable: true` in the session's schema can be set on the chat. A +/// property omitted from `values` takes the session's value. +/// - **With `schema`** (the chat's {@link ChatState.provider | `provider`} +/// differs from the session's): {@link values} is the chat's full effective +/// config and nothing is inherited from the session, whose schema and values +/// belong to the session's agent. The host seeds `values` from the chat +/// agent's defaults when it creates the chat. A property omitted from +/// `values`, including after a `chat/configChanged` with `replace: true`, +/// takes its `default` from `schema`. /// -/// A chat whose {@link ChatState.provider | `provider`} differs from the -/// session's provider does not inherit the session's schema or values: the -/// session's agent owns those. The host MUST set {@link schema} to that chat's -/// agent's chat-applicable config schema and seed {@link values} from that -/// agent's defaults when it creates the chat; those values are the chat's -/// effective config. The chat's settings are its own and change only through -/// `chat/configChanged` on that chat. +/// Settings change only through `chat/configChanged` on the chat, which never +/// changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not +/// valid for the chat's effective schema (`schema` when present, otherwise the +/// session's schema restricted to `chatMutable` properties). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { /// JSON Schema describing the config properties that apply to this chat. /// - /// The host MUST set it when the chat's provider differs from the session's - /// provider and MAY omit it otherwise. When absent, clients MUST use the - /// session's {@link SessionState.config | `SessionState.config.schema`}. - /// When present, clients MUST render and validate this chat's settings - /// against it instead, and only its properties with `chatMutable: true` - /// can be set with `chat/configChanged`. `chat/configChanged` never changes - /// the schema. + /// The host MUST set it if and only if the chat's + /// {@link ChatState.provider | `provider`} differs from the session's + /// provider, to that agent's chat-applicable config schema, and MUST NOT set + /// it otherwise. When absent, clients MUST use the session's + /// {@link SessionState.config | `SessionState.config.schema`}. When present, + /// clients MUST render and validate this chat's settings against it instead, + /// and only its properties with `chatMutable: true` can be set with + /// `chat/configChanged`. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, - /// Config values set on this chat, keyed by property id + /// Config values set on this chat, keyed by property id. An overlay on the + /// session's values when {@link schema} is absent; the chat's full effective + /// config when it is present. pub values: JsonObject, } diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index bbf08d72f..c26b27139 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -1421,6 +1421,7 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu } StateAction::ChatConfigChanged(a) => { let config = state.config.get_or_insert_with(|| ChatConfigState { + schema: None, values: Default::default(), }); if a.replace.unwrap_or(false) { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 9c0cad0d9..b8bab725a 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -472,8 +472,8 @@ public struct SessionChatUpdatedAction: Codable, Sendable { public var chat: String /// Mutable summary fields that changed; omitted fields are unchanged. /// - /// Identity fields (`resource`) never change and MUST be omitted by - /// senders; receivers SHOULD ignore them if present. + /// Identity fields (`resource` and `provider`) never change and MUST be + /// omitted by senders; receivers SHOULD ignore them if present. public var changes: PartialChatSummary public init( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 0dc4ad373..707b78a43 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1829,8 +1829,9 @@ public struct ChatState: Codable, Sendable { /// absent value that way. Hosts MAY omit it when it equals the session's /// provider and MUST set it when it differs, which only happens when the /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's - /// agent never changes; hosts MUST NOT change this value through - /// `session/chatUpdated`. Forks and side chats always use their source + /// agent never changes, including across `moveChat`: like `resource`, it is + /// an identity field that senders MUST omit from `session/chatUpdated` + /// changes and receivers ignore. Forks and side chats always use their source /// chat's agent. /// /// Stability: 1.0 - Early development. @@ -1871,8 +1872,9 @@ public struct ChatState: Codable, Sendable { /// Clients subscribe to each resource for the experimental presentation /// state, including its current live source URL. public var canvases: [CanvasReference]? - /// Configuration values set on this chat, applied over the owning session's - /// {@link SessionState.config | `SessionState.config`}. See + /// Configuration set on this chat: an overlay on the owning session's + /// {@link SessionState.config | `SessionState.config`}, or the chat's own + /// schema and full values when its agent differs from the session's. See /// {@link ChatConfigState}. public var config: ChatConfigState? /// Completed turns @@ -2710,17 +2712,20 @@ public struct SessionConfigState: Codable, Sendable { public struct ChatConfigState: Codable, Sendable { /// JSON Schema describing the config properties that apply to this chat. /// - /// The host MUST set it when the chat's provider differs from the session's - /// provider and MAY omit it otherwise. When absent, clients MUST use the - /// session's {@link SessionState.config | `SessionState.config.schema`}. - /// When present, clients MUST render and validate this chat's settings - /// against it instead, and only its properties with `chatMutable: true` - /// can be set with `chat/configChanged`. `chat/configChanged` never changes - /// the schema. + /// The host MUST set it if and only if the chat's + /// {@link ChatState.provider | `provider`} differs from the session's + /// provider, to that agent's chat-applicable config schema, and MUST NOT set + /// it otherwise. When absent, clients MUST use the session's + /// {@link SessionState.config | `SessionState.config.schema`}. When present, + /// clients MUST render and validate this chat's settings against it instead, + /// and only its properties with `chatMutable: true` can be set with + /// `chat/configChanged`. /// /// Stability: 1.0 - Early development. public var schema: SessionConfigSchema? - /// Config values set on this chat, keyed by property id + /// Config values set on this chat, keyed by property id. An overlay on the + /// session's values when {@link schema} is absent; the chat's full effective + /// config when it is present. public var values: [String: AnyCodable] public init( diff --git a/docs/.changes/20261019-chat-config-schema.json b/docs/.changes/20261019-chat-config-schema.json index 829ca6b17..141d81114 100644 --- a/docs/.changes/20261019-chat-config-schema.json +++ b/docs/.changes/20261019-chat-config-schema.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Optional `ChatConfigState.schema` lets a chat carry its own config schema. Hosts set it for chats backed by a different agent than their session, whose values start from that agent's defaults; when absent, clients use the session's config schema." + "message": "Optional `ChatConfigState.schema` lets a chat carry its own config schema. Hosts set it if and only if the chat's agent differs from its session's; then `values` is the chat's full effective config, seeded from that agent's defaults, with omitted properties taking their schema defaults. Without it, `values` stays an overlay on the session config. `moveChat` normalizes `config` for the destination session." } diff --git a/docs/.changes/20261019-kotlin-positional-chat-fields.json b/docs/.changes/20261019-kotlin-positional-chat-fields.json new file mode 100644 index 000000000..c5327f166 --- /dev/null +++ b/docs/.changes/20261019-kotlin-positional-chat-fields.json @@ -0,0 +1,5 @@ +{ + "type": "changed", + "message": "`ChatState`, `ChatSummary`, `SessionChatSummary`, and `ChatConfigState` gained parameters (`provider`, `schema`), which shifts their positional constructor arguments and `componentN` destructuring; use named arguments.", + "targets": ["kotlin"] +} diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index d362a8694..7a68f080a 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -213,22 +213,24 @@ changeset channel contract. Hosts scope a chat's catalogue to its effective working directories: the chat's subset when present, otherwise the full session set. -`config` is an overlay on `SessionState.config`: it carries only the values set -on this chat, and the session's config schema applies unless the chat carries -its own `config.schema`. A chat's effective -config is the session's values with the chat's values applied over them. Only -properties marked `chatMutable: true` in the session's schema can be set per -chat, with `chat/configChanged`. See +`config` holds the chat's own settings. Without `config.schema` it is an +overlay on `SessionState.config`: it carries only the values set on this chat, +the session's config schema applies, and the chat's effective config is the +session's values with the chat's values applied over them. Only properties +marked `chatMutable: true` in the session's schema can be set per chat, with +`chat/configChanged`. See [Per-chat configuration](/specification/chat-channel#per-chat-configuration). `provider` names the chat's agent. Absence means the session's `provider`. Hosts that advertise `multipleChatProviders` in their `initialize` result may back chats in one session with different agents, chosen by `createChat`'s -`provider`; forks and side chats keep their source chat's agent. A chat whose -agent differs from the session's carries that agent's config schema in -`config.schema` and starts from that agent's default values rather than -inheriting the session's; clients render and validate its settings against -that schema. See +`provider`; forks and side chats keep their source chat's agent, and a chat's +agent never changes. The host sets `config.schema` if and only if a chat's +agent differs from the session's. Such a chat inherits nothing from the +session: `config.values` is its full effective config, seeded from that +agent's defaults, and omitted properties take their `schema` defaults. Clients +render and validate its settings against that schema. `moveChat` normalizes +`config` when a chat's agent relationship to its session changes. See [Chat providers](/specification/chat-channel#chat-providers). Fork and side-chat creation both reference source turns by stable identifiers. diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 9f60ad718..c923f5711 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -78,33 +78,37 @@ fully replaces the catalogue or clears it when `changesets` is absent. ### Per-chat configuration -[`ChatState.config`](/reference/chat#chatstate) is an overlay on the owning -session's [`SessionState.config`](/reference/session#sessionstate). It carries -only the values set on this chat and, optionally, a `schema`. When `schema` is -absent, the session's config schema applies. A -chat's effective config is the session's values with the chat's values -applied over them. Hosts do not mirror session config into chats. - -A host opts a property in to per-chat values by setting +[`ChatState.config`](/reference/chat#chatstate) holds a chat's own settings. +Its meaning depends on whether it carries a `schema`. The host MUST set +`schema` if and only if the chat's agent differs from the session's agent (see +[Chat providers](#chat-providers)), and MUST NOT set it otherwise. + +**Without `schema`**, `config` is an overlay on the owning session's +[`SessionState.config`](/reference/session#sessionstate). It carries only the +values set on this chat; the session's config schema applies. A chat's +effective config is the session's values with the chat's values applied over +them. Hosts do not mirror session config into chats. A host opts a property in +to per-chat values by setting [`chatMutable: true`](/reference/session#sessionconfigpropertyschema) on it in -the session's config schema. Clients set such properties for one chat by -dispatching [`chat/configChanged`](/reference/chat#chatconfigchangedaction), -which merges into the chat's overlay or replaces it when `replace` is `true`. -To reset a property to the session's value, replace the overlay with one that -omits the property. Every other property is changed on the session with -`session/configChanged`. - -A chat backed by a different agent than its session (see -[Chat providers](#chat-providers)) does not inherit the session's schema or -values, which belong to the session's agent. The host MUST set -`ChatState.config.schema` to that agent's chat-applicable config schema and -seed `ChatState.config.values` from that agent's defaults when it creates the -chat; those values are the chat's effective config. When a chat uses the -session's agent, the host MAY omit `schema`, and clients MUST then use the -session's `SessionState.config.schema`. Whenever `schema` is present, clients -MUST render and validate the chat's settings against it rather than the -session's schema. The chat's settings change only through `chat/configChanged` -on that chat, which never changes the schema. +the session's config schema. To reset a property to the session's value, +replace the overlay with one that omits the property. Every other property is +changed on the session with `session/configChanged`. + +**With `schema`**, the chat is backed by a different agent than its session +and inherits nothing from the session, whose schema and values belong to the +session's agent. `schema` is that agent's chat-applicable config schema, and +`values` is the chat's full effective config, seeded from that agent's +defaults when the host creates the chat. A property omitted from `values`, +including after a replace, takes its `default` from `schema`. Clients MUST +render and validate the chat's settings against `schema` rather than the +session's schema; only its `chatMutable` properties can be set on the chat. + +In both cases clients change a chat's settings by dispatching +[`chat/configChanged`](/reference/chat#chatconfigchangedaction) on that chat, +which merges into `values` or replaces them when `replace` is `true`, and never +changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not +valid for the chat's effective schema: `schema` when present, otherwise the +session's schema restricted to `chatMutable` properties. ### Chat providers @@ -120,7 +124,9 @@ chat. A host that advertises `ChatState` and on [`SessionChatSummary`](/reference/session#sessionchatsummary)) reports a chat's agent. Absence means the session's `provider`; clients MUST treat it that way. Hosts MAY omit it when it equals the session's provider - and MUST set it when it differs. A chat's agent never changes. + and MUST set it when it differs. A chat's agent never changes: `provider` + is an identity field, like `resource`, that senders MUST omit from + `session/chatUpdated` changes and receivers ignore. - A client MAY pass [`provider`](/reference/chat#createchatparams) to `createChat` only when the host advertises `multipleChatProviders`. When it is absent, the new chat uses the source chat's agent for forks and side chats, @@ -137,8 +143,13 @@ chat. A host that advertises and agent-default values in `ChatState.config`; see [Per-chat configuration](#per-chat-configuration). - Hosts MUST NOT advertise `multipleChatProviders` on connections that - negotiated a protocol version earlier than `1.1.0`. Clients and hosts that do - not use the capability are unaffected: every chat uses the session's agent. + negotiated a protocol version earlier than `1.1.0`. Hosts that do not + advertise it keep every chat on the session's agent. A client that negotiates + `1.1.0` or later but does not understand `provider` or `config.schema` shows + such chats as using the session's agent and may present the session's + settings for them; hosts still reject `chat/configChanged` keys that are not + valid for the chat's effective schema, so such a client cannot apply + settings the chat's agent does not accept. ## Relationship to the session channel @@ -268,7 +279,8 @@ anchor. Hosts publish `chat/movableChanged` and the corresponding ``` The result always reports the authoritative destination session. Every moved -chat keeps its URI, state, and byte-for-byte `ChatOrigin`, so existing +chat keeps its URI, state (apart from the `config` normalization described +under [Validation](#validation)), and byte-for-byte `ChatOrigin`, so existing chat-channel subscriptions remain valid. #### Validation @@ -292,6 +304,24 @@ that does not advertise the capability cannot report a per-chat agent, so it MAY reject, as an unsupported destination, a move into a session with a different `provider`. See [Chat providers](#chat-providers). +A moved chat always keeps its `provider`. Because no action changes +`ChatState.provider`, a host that advertises `multipleChatProviders` MUST set +`provider` on the `ChatState` of every chat it advertises as `movable`. When +ownership changes, such a host MUST normalize each moved chat's +`ChatState.config` for the destination session as part of the committed move: + +- If the chat's agent differs from the destination session's `provider`, + `config.schema` is the chat agent's chat-applicable config schema and + `config.values` is that agent's defaults overlaid with the chat's existing + values that are valid in that schema. +- If they are equal, `config.schema` is removed and `config.values` keeps, as + an overlay, only the existing values whose properties the destination + session's schema declares `chatMutable`. + +No action carries a changed `schema`, so clients subscribed to a moved chat +whose agent relationship to its session changed SHOULD resubscribe to obtain +the normalized `config`. See [Per-chat configuration](#per-chat-configuration). + #### Atomic synchronization The host validates and persists ownership, its internal hierarchy, catalog diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 48ad267e5..c6ff780b2 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -192,7 +192,7 @@ }, "SessionChatUpdatedAction": { "type": "object", - "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`, `provider`) MUST\nNOT be carried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", "properties": { "type": { "const": "session/chatUpdated" @@ -252,7 +252,7 @@ "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." + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource` and `provider`) never change and MUST be\nomitted by senders; receivers SHOULD ignore them if present." } }, "required": [ @@ -5230,7 +5230,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -5262,7 +5262,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -5492,16 +5492,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index fa32405e2..e60864ca9 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1483,7 +1483,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of every chat it advertises as `movable`.\nWhen ownership changes, such a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move:\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values whose\n properties the destination session's schema declares `chatMutable`.\n\nNo action carries a changed `schema`, so clients subscribed to a moved chat\nwhose provider relationship to its session changed SHOULD resubscribe to\nobtain the normalized `config`.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -4515,7 +4515,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -4547,7 +4547,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -4777,16 +4777,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." } }, "required": [ @@ -8169,7 +8169,7 @@ }, "SessionChatUpdatedAction": { "type": "object", - "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`, `provider`) MUST\nNOT be carried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", "properties": { "type": { "const": "session/chatUpdated" @@ -8229,7 +8229,7 @@ "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." + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource` and `provider`) never change and MUST be\nomitted by senders; receivers SHOULD ignore them if present." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index b9c9bb69a..0955070c6 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2640,7 +2640,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2672,7 +2672,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -2902,16 +2902,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." } }, "required": [ @@ -7585,7 +7585,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of every chat it advertises as `movable`.\nWhen ownership changes, such a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move:\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values whose\n properties the destination session's schema declares `chatMutable`.\n\nNo action carries a changed `schema`, so clients subscribed to a moved chat\nwhose provider relationship to its session changed SHOULD resubscribe to\nobtain the normalized `config`.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -9359,7 +9359,7 @@ }, "SessionChatUpdatedAction": { "type": "object", - "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`, `provider`) MUST\nNOT be carried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", "properties": { "type": { "const": "session/chatUpdated" @@ -9419,7 +9419,7 @@ "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." + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource` and `provider`) never change and MUST be\nomitted by senders; receivers SHOULD ignore them if present." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 51c13bc70..ec6552b0b 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2942,7 +2942,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2974,7 +2974,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -3204,16 +3204,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 5a8922a02..1f08f0b8b 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2551,7 +2551,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes; hosts MUST NOT change this value through\n`session/chatUpdated`. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2583,7 +2583,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -2813,16 +2813,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values. When {@link schema} is absent, the\nsession's config schema applies: the effective config of a chat is the\nsession's values with these values applied over them, and only properties\nwith `chatMutable: true` in the session's config schema can be set on a\nchat.\n\nA chat whose {@link ChatState.provider | `provider`} differs from the\nsession's provider does not inherit the session's schema or values: the\nsession's agent owns those. The host MUST set {@link schema} to that chat's\nagent's chat-applicable config schema and seed {@link values} from that\nagent's defaults when it creates the chat; those values are the chat's\neffective config. The chat's settings are its own and change only through\n`chat/configChanged` on that chat.", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it when the chat's provider differs from the session's\nprovider and MAY omit it otherwise. When absent, clients MUST use the\nsession's {@link SessionState.config | `SessionState.config.schema`}.\nWhen present, clients MUST render and validate this chat's settings\nagainst it instead, and only its properties with `chatMutable: true`\ncan be set with `chat/configChanged`. `chat/configChanged` never changes\nthe schema.\n\nStability: 1.0 - Early development." + "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." } }, "required": [ diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 7bc4f6fee..21706eee2 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -225,8 +225,28 @@ export type ChatMoveDestination = * destination, a move that would place a chat in a session with a different * provider. * - * On success every moved chat keeps its URI, state, and immutable - * `ChatOrigin`. The host commits ownership and catalog order before publishing + * A moved chat always keeps its `provider`. Because no action changes + * `ChatState.provider`, a host that advertises the capability MUST set + * `provider` on the `ChatState` of every chat it advertises as `movable`. + * When ownership changes, such a host MUST normalize each moved chat's + * {@link ChatConfigState | `ChatState.config`} for the destination session as + * part of the committed move: + * + * - If the chat's provider differs from the destination session's provider, + * `config.schema` is the chat agent's chat-applicable config schema and + * `config.values` is that agent's defaults overlaid with the chat's existing + * values that are valid in that schema. + * - If the providers are equal, `config.schema` is removed and + * `config.values` keeps, as an overlay, only the existing values whose + * properties the destination session's schema declares `chatMutable`. + * + * No action carries a changed `schema`, so clients subscribed to a moved chat + * whose provider relationship to its session changed SHOULD resubscribe to + * obtain the normalized `config`. + * + * On success every moved chat keeps its URI, its state (apart from the + * `config` normalization above), and its immutable `ChatOrigin`. The host + * commits ownership and catalog order before publishing * `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and * root summary updates as applicable. Session and root snapshots are the * durable recovery path after reconnect or an uncertain response. diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 4aa80bfdb..9ce18848f 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -91,8 +91,9 @@ export interface ChatState { * absent value that way. Hosts MAY omit it when it equals the session's * provider and MUST set it when it differs, which only happens when the * host advertises {@link InitializeResult.multipleChatProviders}. A chat's - * agent never changes; hosts MUST NOT change this value through - * `session/chatUpdated`. Forks and side chats always use their source + * agent never changes, including across `moveChat`: like `resource`, it is + * an identity field that senders MUST omit from `session/chatUpdated` + * changes and receivers ignore. Forks and side chats always use their source * chat's agent. * @stability 1.0 */ @@ -142,8 +143,9 @@ export interface ChatState { */ canvases?: CanvasReference[]; /** - * Configuration values set on this chat, applied over the owning session's - * {@link SessionState.config | `SessionState.config`}. See + * Configuration set on this chat: an overlay on the owning session's + * {@link SessionState.config | `SessionState.config`}, or the chat's own + * schema and full values when its agent differs from the session's. See * {@link ChatConfigState}. */ config?: ChatConfigState; @@ -439,22 +441,28 @@ export const enum ChatInteractivity { // ─── Chat Config Types ─────────────────────────────────────────────────────── /** - * Configuration values set on a chat: an overlay on the owning session's - * {@link SessionState.config | `SessionState.config`}. - * - * It carries only the chat's own values. When {@link schema} is absent, the - * session's config schema applies: the effective config of a chat is the - * session's values with these values applied over them, and only properties - * with `chatMutable: true` in the session's config schema can be set on a - * chat. - * - * A chat whose {@link ChatState.provider | `provider`} differs from the - * session's provider does not inherit the session's schema or values: the - * session's agent owns those. The host MUST set {@link schema} to that chat's - * agent's chat-applicable config schema and seed {@link values} from that - * agent's defaults when it creates the chat; those values are the chat's - * effective config. The chat's settings are its own and change only through - * `chat/configChanged` on that chat. + * Configuration set on a chat. Its meaning depends on whether {@link schema} + * is present. + * + * - **Without `schema`** (the chat uses the session's agent): {@link values} + * is an overlay on the owning session's + * {@link SessionState.config | `SessionState.config`}. The session's config + * schema applies; the chat's effective config is the session's values with + * these values applied over them, and only properties with + * `chatMutable: true` in the session's schema can be set on the chat. A + * property omitted from `values` takes the session's value. + * - **With `schema`** (the chat's {@link ChatState.provider | `provider`} + * differs from the session's): {@link values} is the chat's full effective + * config and nothing is inherited from the session, whose schema and values + * belong to the session's agent. The host seeds `values` from the chat + * agent's defaults when it creates the chat. A property omitted from + * `values`, including after a `chat/configChanged` with `replace: true`, + * takes its `default` from `schema`. + * + * Settings change only through `chat/configChanged` on the chat, which never + * changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not + * valid for the chat's effective schema (`schema` when present, otherwise the + * session's schema restricted to `chatMutable` properties). * * @category Chat Config Types */ @@ -462,17 +470,22 @@ export interface ChatConfigState { /** * JSON Schema describing the config properties that apply to this chat. * - * The host MUST set it when the chat's provider differs from the session's - * provider and MAY omit it otherwise. When absent, clients MUST use the - * session's {@link SessionState.config | `SessionState.config.schema`}. - * When present, clients MUST render and validate this chat's settings - * against it instead, and only its properties with `chatMutable: true` - * can be set with `chat/configChanged`. `chat/configChanged` never changes - * the schema. + * The host MUST set it if and only if the chat's + * {@link ChatState.provider | `provider`} differs from the session's + * provider, to that agent's chat-applicable config schema, and MUST NOT set + * it otherwise. When absent, clients MUST use the session's + * {@link SessionState.config | `SessionState.config.schema`}. When present, + * clients MUST render and validate this chat's settings against it instead, + * and only its properties with `chatMutable: true` can be set with + * `chat/configChanged`. * @stability 1.0 */ schema?: SessionConfigSchema; - /** Config values set on this chat, keyed by property id */ + /** + * Config values set on this chat, keyed by property id. An overlay on the + * session's values when {@link schema} is absent; the chat's full effective + * config when it is present. + */ values: Record; } diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 9e54a3319..bf19dcf3c 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -76,8 +76,8 @@ export interface SessionChatRemovedAction { * One existing chat's summary fields changed. * * Partial-update semantics: only fields present in `changes` are written; - * omitted fields are preserved. Identity fields (`resource`) MUST NOT be - * carried in `changes`. No-op when no entry with `chat` exists — clients + * omitted fields are preserved. Identity fields (`resource`, `provider`) MUST + * NOT be carried in `changes`. No-op when no entry with `chat` exists — clients * SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. * * Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -95,8 +95,8 @@ export interface SessionChatUpdatedAction { /** * Mutable summary fields that changed; omitted fields are unchanged. * - * Identity fields (`resource`) never change and MUST be omitted by - * senders; receivers SHOULD ignore them if present. + * Identity fields (`resource` and `provider`) never change and MUST be + * omitted by senders; receivers SHOULD ignore them if present. */ changes: Partial; } diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index b18307522..fdac22908 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -187,7 +187,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: if (idx < 0) { return state; } - const { resource: _ignored, ...changes } = action.changes; + const { resource: _resource, provider: _provider, ...changes } = action.changes; const updated = list.slice(); updated[idx] = { ...list[idx], ...changes }; return { ...state, chats: updated }; diff --git a/types/test-cases/reducers/309-session-chatupdated-ignores-provider.json b/types/test-cases/reducers/309-session-chatupdated-ignores-provider.json new file mode 100644 index 000000000..89015f480 --- /dev/null +++ b/types/test-cases/reducers/309-session-chatupdated-ignores-provider.json @@ -0,0 +1,46 @@ +{ + "description": "session/chatUpdated ignores the provider identity field", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [ + { + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "provider": "other-agent" + } + ] + }, + "actions": [ + { + "type": "session/chatUpdated", + "chat": "ahp-chat:/c2", + "changes": { + "title": "Renamed", + "provider": "copilot" + } + } + ], + "expected": { + "provider": "copilot", + "title": "Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [ + { + "resource": "ahp-chat:/c2", + "title": "Renamed", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "provider": "other-agent" + } + ] + } +} diff --git a/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json b/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json new file mode 100644 index 000000000..c2ad9fa6b --- /dev/null +++ b/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json @@ -0,0 +1,57 @@ +{ + "description": "chat/configChanged with replace preserves the chat's config schema", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": true + } + } + }, + "actions": [ + { + "type": "chat/configChanged", + "config": {}, + "replace": true + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": {} + } + } +} From e1ff051f3007fe7aecb5abd27c53938bf29218e7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Oct 2026 17:19:30 +0200 Subject: [PATCH 04/13] rust: set `provider` in reducer test fixtures Add `provider: None` to the `ChatState` and `ChatSummary` initializers in the ahp crate's reducer tests, which stopped compiling when those structs gained the optional `provider` field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/rust/crates/ahp/src/reducers.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index c26b27139..79cae8a8f 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -2325,6 +2325,7 @@ mod tests { origin: None, movable: None, interactivity: None, + provider: None, working_directories: None, changesets: None, canvases: None, @@ -2486,6 +2487,7 @@ mod tests { origin: None, movable: None, interactivity: None, + provider: None, working_directories: None, }; let added = StateAction::SessionChatAdded(ahp_types::actions::SessionChatAddedAction { From 0c5b0e66bfd87742e6b18ede685cc20edf04a195 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Oct 2026 18:59:25 +0200 Subject: [PATCH 05/13] chat: clarify per-chat provider spec from PR review Spec text only; no protocol shape changes. - Pre-1.1.0 connections: only a host advertising `multipleChatProviders` on some connection creates mixed-agent chats; other connections see `provider`/`schema` as ignored unknown fields. - moveChat normalization: hosts publish `chat/configChanged` with `replace: true` when values change, clients resubscribe when `schema` changes, hosts MAY refuse moves needing normalization, and `schema` is otherwise fixed. Define "valid" (key and value), state that non-chatMutable session values don't carry over, and cover `newSession`. - `ChatState.provider` is required on movable chats only when the host accepts cross-provider moves for them. - configChanged rejection uses `schema`'s chatMutable properties; with a schema, `replace` may only change or drop chatMutable keys. - "receivers SHOULD ignore" wording made consistent. - Rust change fragment for struct literals; fixtures 080 (session catalog provider) and 311 (replace with absent config). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Commands.generated.cs | 45 ++++++++---- .../Generated/State.generated.cs | 26 ++++--- clients/go/ahptypes/commands.generated.go | 45 ++++++++---- clients/go/ahptypes/state.generated.go | 26 ++++--- .../generated/Commands.generated.kt | 11 ++- .../generated/State.generated.kt | 14 ++-- clients/rust/crates/ahp-types/src/commands.rs | 45 ++++++++---- clients/rust/crates/ahp-types/src/state.rs | 26 ++++--- .../Generated/Commands.generated.swift | 11 ++- .../Generated/State.generated.swift | 14 ++-- .../20261020-rust-chat-struct-literals.json | 5 ++ docs/specification/chat-channel.md | 72 ++++++++++++------- schema/actions.schema.json | 4 +- schema/commands.schema.json | 8 +-- schema/errors.schema.json | 8 +-- schema/notifications.schema.json | 4 +- schema/state.schema.json | 4 +- types/channels-chat/commands.ts | 32 ++++++--- types/channels-chat/state.ts | 26 ++++--- types/common/commands.ts | 11 ++- ...ed-replace-creates-config-when-absent.json | 34 +++++++++ ...sion-chat-summary-provider-round-trip.json | 52 ++++++++++++++ 22 files changed, 386 insertions(+), 137 deletions(-) create mode 100644 docs/.changes/20261020-rust-chat-struct-literals.json create mode 100644 types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json create mode 100644 types/test-cases/round-trips/080-session-chat-summary-provider-round-trip.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 5e35b10ca..708b4a967 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -440,14 +440,19 @@ public sealed record InitializeResult /// Present when the host lets chats in one session be backed by different /// agents. Clients MAY then pass {@link CreateChatParams.provider} to /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that - /// differs from the session's {@link SessionMetadata.provider}. When absent, - /// every chat uses its session's provider and clients MUST NOT send + /// differs from the session's {@link SessionMetadata.provider}. When absent + /// on a connection, clients on that connection MUST NOT send /// {@link CreateChatParams.provider}. /// /// This is a host capability rather than an {@link AgentCapabilities | agent /// capability} because the host, not an individual agent, decides which /// agents may share a session. Hosts MUST NOT advertise it on connections - /// that negotiated a protocol version earlier than `1.1.0`. + /// that negotiated a protocol version earlier than `1.1.0`. Only a host that + /// advertises it on some connection creates chats whose provider differs from + /// their session's; such chats are shared state, so clients on other + /// connections, including pre-`1.1.0` connections, can observe them. On those + /// connections `provider` and `ChatConfigState.schema` arrive as unknown + /// fields and are ignored, so the chats appear to use the session's agent. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1096,22 +1101,36 @@ public sealed record ChatMoveToNewSessionDestination /// /// A moved chat always keeps its `provider`. Because no action changes /// `ChatState.provider`, a host that advertises the capability MUST set -/// `provider` on the `ChatState` of every chat it advertises as `movable`. -/// When ownership changes, such a host MUST normalize each moved chat's +/// `provider` on the `ChatState` of a `movable` chat if it accepts moves of +/// that chat into sessions with a different provider; otherwise the +/// {@link ChatState.provider} omission rule applies. When ownership changes, +/// such a host MUST normalize each moved chat's /// {@link ChatConfigState | `ChatState.config`} for the destination session as -/// part of the committed move: +/// part of the committed move. A value is valid in a schema when its key is a +/// property of that schema and its value matches the property's type and any +/// `enum`. /// /// - If the chat's provider differs from the destination session's provider, /// `config.schema` is the chat agent's chat-applicable config schema and /// `config.values` is that agent's defaults overlaid with the chat's existing -/// values that are valid in that schema. +/// values that are valid in that schema. Session-level values that are not +/// `chatMutable` do not carry over. /// - If the providers are equal, `config.schema` is removed and -/// `config.values` keeps, as an overlay, only the existing values whose -/// properties the destination session's schema declares `chatMutable`. -/// -/// No action carries a changed `schema`, so clients subscribed to a moved chat -/// whose provider relationship to its session changed SHOULD resubscribe to -/// obtain the normalized `config`. +/// `config.values` keeps, as an overlay, only the existing values that are +/// valid in the destination session's schema and whose properties it +/// declares `chatMutable`. +/// - For a `newSession` destination, the new session's `provider` is the +/// requested chat's provider, its `config` is seeded from that chat's +/// effective config values, and the chat drops `schema`, keeping as an +/// overlay only its values that are `chatMutable` in the new session's +/// schema. +/// +/// Whenever normalization changes a moved chat's `config.values`, the host +/// MUST publish `chat/configChanged` with `replace: true` and the normalized +/// values on that chat's channel. No action carries a changed `schema`, so +/// clients subscribed to a moved chat whose `config.schema` changed SHOULD +/// resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, +/// any move that would require normalization. /// /// On success every moved chat keeps its URI, its state (apart from the /// `config` normalization above), and its immutable `ChatOrigin`. The host diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 7eb94db13..d68461006 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2505,11 +2505,15 @@ public sealed class ChatState /// Absence means the owning session's /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an /// absent value that way. Hosts MAY omit it when it equals the session's - /// provider and MUST set it when it differs, which only happens when the - /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's - /// agent never changes, including across `moveChat`: like `resource`, it is - /// an identity field that senders MUST omit from `session/chatUpdated` - /// changes and receivers ignore. Forks and side chats always use their source + /// provider and MUST set it when it differs, which only happens on a host + /// that advertises {@link InitializeResult.multipleChatProviders} on some + /// connection. Such a host MUST also set it on a movable chat whose + /// cross-provider moves it accepts (see `moveChat`). Clients that do not + /// understand this field, including clients on pre-`1.1.0` connections, + /// ignore it and show the chat as using the session's agent. A chat's agent + /// never changes, including across `moveChat`: like `resource`, it is an + /// identity field that senders MUST omit from `session/chatUpdated` changes + /// and receivers SHOULD ignore. Forks and side chats always use their source /// chat's agent. /// /// Stability: 1.0 - Early development. @@ -3555,9 +3559,15 @@ public sealed class SessionConfigState /// takes its `default` from `schema`. /// /// Settings change only through `chat/configChanged` on the chat, which never -/// changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not -/// valid for the chat's effective schema (`schema` when present, otherwise the -/// session's schema restricted to `chatMutable` properties). +/// changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only +/// `moveChat` normalization can change it. Hosts MUST reject +/// `chat/configChanged` keys that are not valid for the chat's effective +/// schema: `schema`'s `chatMutable` properties when present, otherwise the +/// session's `chatMutable` properties. With `schema`, a `replace` may only +/// change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` +/// value unchanged (hosts accept those keys when their value is unchanged and +/// reject the request otherwise), so those values stay at whatever the host +/// set. public sealed class ChatConfigState { /// JSON Schema describing the config properties that apply to this chat. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index c59e776ca..cc190a332 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -227,14 +227,19 @@ type InitializeResult struct { // Present when the host lets chats in one session be backed by different // agents. Clients MAY then pass {@link CreateChatParams.provider} to // `createChat`, and chats MAY report a {@link ChatSummary.provider} that - // differs from the session's {@link SessionMetadata.provider}. When absent, - // every chat uses its session's provider and clients MUST NOT send + // differs from the session's {@link SessionMetadata.provider}. When absent + // on a connection, clients on that connection MUST NOT send // {@link CreateChatParams.provider}. // // This is a host capability rather than an {@link AgentCapabilities | agent // capability} because the host, not an individual agent, decides which // agents may share a session. Hosts MUST NOT advertise it on connections - // that negotiated a protocol version earlier than `1.1.0`. + // that negotiated a protocol version earlier than `1.1.0`. Only a host that + // advertises it on some connection creates chats whose provider differs from + // their session's; such chats are shared state, so clients on other + // connections, including pre-`1.1.0` connections, can observe them. On those + // connections `provider` and `ChatConfigState.schema` arrive as unknown + // fields and are ignored, so the chats appear to use the session's agent. // // Stability: 1.0 - Early development. MultipleChatProviders *MultipleChatProvidersCapability `json:"multipleChatProviders,omitempty"` @@ -761,22 +766,36 @@ type ChatMoveToNewSessionDestination struct { // // A moved chat always keeps its `provider`. Because no action changes // `ChatState.provider`, a host that advertises the capability MUST set -// `provider` on the `ChatState` of every chat it advertises as `movable`. -// When ownership changes, such a host MUST normalize each moved chat's +// `provider` on the `ChatState` of a `movable` chat if it accepts moves of +// that chat into sessions with a different provider; otherwise the +// {@link ChatState.provider} omission rule applies. When ownership changes, +// such a host MUST normalize each moved chat's // {@link ChatConfigState | `ChatState.config`} for the destination session as -// part of the committed move: +// part of the committed move. A value is valid in a schema when its key is a +// property of that schema and its value matches the property's type and any +// `enum`. // // - If the chat's provider differs from the destination session's provider, // `config.schema` is the chat agent's chat-applicable config schema and // `config.values` is that agent's defaults overlaid with the chat's existing -// values that are valid in that schema. +// values that are valid in that schema. Session-level values that are not +// `chatMutable` do not carry over. // - If the providers are equal, `config.schema` is removed and -// `config.values` keeps, as an overlay, only the existing values whose -// properties the destination session's schema declares `chatMutable`. -// -// No action carries a changed `schema`, so clients subscribed to a moved chat -// whose provider relationship to its session changed SHOULD resubscribe to -// obtain the normalized `config`. +// `config.values` keeps, as an overlay, only the existing values that are +// valid in the destination session's schema and whose properties it +// declares `chatMutable`. +// - For a `newSession` destination, the new session's `provider` is the +// requested chat's provider, its `config` is seeded from that chat's +// effective config values, and the chat drops `schema`, keeping as an +// overlay only its values that are `chatMutable` in the new session's +// schema. +// +// Whenever normalization changes a moved chat's `config.values`, the host +// MUST publish `chat/configChanged` with `replace: true` and the normalized +// values on that chat's channel. No action carries a changed `schema`, so +// clients subscribed to a moved chat whose `config.schema` changed SHOULD +// resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, +// any move that would require normalization. // // On success every moved chat keeps its URI, its state (apart from the // `config` normalization above), and its immutable `ChatOrigin`. The host diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index df05b2f36..81c2f96c4 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1409,11 +1409,15 @@ type ChatState struct { // Absence means the owning session's // {@link SessionMetadata.provider | `provider`}; clients MUST treat an // absent value that way. Hosts MAY omit it when it equals the session's - // provider and MUST set it when it differs, which only happens when the - // host advertises {@link InitializeResult.multipleChatProviders}. A chat's - // agent never changes, including across `moveChat`: like `resource`, it is - // an identity field that senders MUST omit from `session/chatUpdated` - // changes and receivers ignore. Forks and side chats always use their source + // provider and MUST set it when it differs, which only happens on a host + // that advertises {@link InitializeResult.multipleChatProviders} on some + // connection. Such a host MUST also set it on a movable chat whose + // cross-provider moves it accepts (see `moveChat`). Clients that do not + // understand this field, including clients on pre-`1.1.0` connections, + // ignore it and show the chat as using the session's agent. A chat's agent + // never changes, including across `moveChat`: like `resource`, it is an + // identity field that senders MUST omit from `session/chatUpdated` changes + // and receivers SHOULD ignore. Forks and side chats always use their source // chat's agent. // // Stability: 1.0 - Early development. @@ -1744,9 +1748,15 @@ type SessionConfigState struct { // takes its `default` from `schema`. // // Settings change only through `chat/configChanged` on the chat, which never -// changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not -// valid for the chat's effective schema (`schema` when present, otherwise the -// session's schema restricted to `chatMutable` properties). +// changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only +// `moveChat` normalization can change it. Hosts MUST reject +// `chat/configChanged` keys that are not valid for the chat's effective +// schema: `schema`'s `chatMutable` properties when present, otherwise the +// session's `chatMutable` properties. With `schema`, a `replace` may only +// change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` +// value unchanged (hosts accept those keys when their value is unchanged and +// reject the request otherwise), so those values stay at whatever the host +// set. type ChatConfigState struct { // JSON Schema describing the config properties that apply to this chat. // 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 353d42687..f32e85380 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 @@ -471,14 +471,19 @@ data class InitializeResult( * Present when the host lets chats in one session be backed by different * agents. Clients MAY then pass {@link CreateChatParams.provider} to * `createChat`, and chats MAY report a {@link ChatSummary.provider} that - * differs from the session's {@link SessionMetadata.provider}. When absent, - * every chat uses its session's provider and clients MUST NOT send + * differs from the session's {@link SessionMetadata.provider}. When absent + * on a connection, clients on that connection MUST NOT send * {@link CreateChatParams.provider}. * * This is a host capability rather than an {@link AgentCapabilities | agent * capability} because the host, not an individual agent, decides which * agents may share a session. Hosts MUST NOT advertise it on connections - * that negotiated a protocol version earlier than `1.1.0`. + * that negotiated a protocol version earlier than `1.1.0`. Only a host that + * advertises it on some connection creates chats whose provider differs from + * their session's; such chats are shared state, so clients on other + * connections, including pre-`1.1.0` connections, can observe them. On those + * connections `provider` and `ChatConfigState.schema` arrive as unknown + * fields and are ignored, so the chats appear to use the session's agent. * * Stability: 1.0 - Early development. */ 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 20af581a0..06be0d2bb 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 @@ -1817,11 +1817,15 @@ data class ChatState( * Absence means the owning session's * {@link SessionMetadata.provider | `provider`}; clients MUST treat an * absent value that way. Hosts MAY omit it when it equals the session's - * provider and MUST set it when it differs, which only happens when the - * host advertises {@link InitializeResult.multipleChatProviders}. A chat's - * agent never changes, including across `moveChat`: like `resource`, it is - * an identity field that senders MUST omit from `session/chatUpdated` - * changes and receivers ignore. Forks and side chats always use their source + * provider and MUST set it when it differs, which only happens on a host + * that advertises {@link InitializeResult.multipleChatProviders} on some + * connection. Such a host MUST also set it on a movable chat whose + * cross-provider moves it accepts (see `moveChat`). Clients that do not + * understand this field, including clients on pre-`1.1.0` connections, + * ignore it and show the chat as using the session's agent. A chat's agent + * never changes, including across `moveChat`: like `resource`, it is an + * identity field that senders MUST omit from `session/chatUpdated` changes + * and receivers SHOULD ignore. Forks and side chats always use their source * chat's agent. * * Stability: 1.0 - Early development. diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 875b32a46..2ce82165a 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -372,14 +372,19 @@ pub struct InitializeResult { /// Present when the host lets chats in one session be backed by different /// agents. Clients MAY then pass {@link CreateChatParams.provider} to /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that - /// differs from the session's {@link SessionMetadata.provider}. When absent, - /// every chat uses its session's provider and clients MUST NOT send + /// differs from the session's {@link SessionMetadata.provider}. When absent + /// on a connection, clients on that connection MUST NOT send /// {@link CreateChatParams.provider}. /// /// This is a host capability rather than an {@link AgentCapabilities | agent /// capability} because the host, not an individual agent, decides which /// agents may share a session. Hosts MUST NOT advertise it on connections - /// that negotiated a protocol version earlier than `1.1.0`. + /// that negotiated a protocol version earlier than `1.1.0`. Only a host that + /// advertises it on some connection creates chats whose provider differs from + /// their session's; such chats are shared state, so clients on other + /// connections, including pre-`1.1.0` connections, can observe them. On those + /// connections `provider` and `ChatConfigState.schema` arrive as unknown + /// fields and are ignored, so the chats appear to use the session's agent. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1021,22 +1026,36 @@ pub struct ChatMoveToNewSessionDestination {} /// /// A moved chat always keeps its `provider`. Because no action changes /// `ChatState.provider`, a host that advertises the capability MUST set -/// `provider` on the `ChatState` of every chat it advertises as `movable`. -/// When ownership changes, such a host MUST normalize each moved chat's +/// `provider` on the `ChatState` of a `movable` chat if it accepts moves of +/// that chat into sessions with a different provider; otherwise the +/// {@link ChatState.provider} omission rule applies. When ownership changes, +/// such a host MUST normalize each moved chat's /// {@link ChatConfigState | `ChatState.config`} for the destination session as -/// part of the committed move: +/// part of the committed move. A value is valid in a schema when its key is a +/// property of that schema and its value matches the property's type and any +/// `enum`. /// /// - If the chat's provider differs from the destination session's provider, /// `config.schema` is the chat agent's chat-applicable config schema and /// `config.values` is that agent's defaults overlaid with the chat's existing -/// values that are valid in that schema. +/// values that are valid in that schema. Session-level values that are not +/// `chatMutable` do not carry over. /// - If the providers are equal, `config.schema` is removed and -/// `config.values` keeps, as an overlay, only the existing values whose -/// properties the destination session's schema declares `chatMutable`. -/// -/// No action carries a changed `schema`, so clients subscribed to a moved chat -/// whose provider relationship to its session changed SHOULD resubscribe to -/// obtain the normalized `config`. +/// `config.values` keeps, as an overlay, only the existing values that are +/// valid in the destination session's schema and whose properties it +/// declares `chatMutable`. +/// - For a `newSession` destination, the new session's `provider` is the +/// requested chat's provider, its `config` is seeded from that chat's +/// effective config values, and the chat drops `schema`, keeping as an +/// overlay only its values that are `chatMutable` in the new session's +/// schema. +/// +/// Whenever normalization changes a moved chat's `config.values`, the host +/// MUST publish `chat/configChanged` with `replace: true` and the normalized +/// values on that chat's channel. No action carries a changed `schema`, so +/// clients subscribed to a moved chat whose `config.schema` changed SHOULD +/// resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, +/// any move that would require normalization. /// /// On success every moved chat keeps its URI, its state (apart from the /// `config` normalization above), and its immutable `ChatOrigin`. The host diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 62f760dbd..dbe988945 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2068,11 +2068,15 @@ pub struct ChatState { /// Absence means the owning session's /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an /// absent value that way. Hosts MAY omit it when it equals the session's - /// provider and MUST set it when it differs, which only happens when the - /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's - /// agent never changes, including across `moveChat`: like `resource`, it is - /// an identity field that senders MUST omit from `session/chatUpdated` - /// changes and receivers ignore. Forks and side chats always use their source + /// provider and MUST set it when it differs, which only happens on a host + /// that advertises {@link InitializeResult.multipleChatProviders} on some + /// connection. Such a host MUST also set it on a movable chat whose + /// cross-provider moves it accepts (see `moveChat`). Clients that do not + /// understand this field, including clients on pre-`1.1.0` connections, + /// ignore it and show the chat as using the session's agent. A chat's agent + /// never changes, including across `moveChat`: like `resource`, it is an + /// identity field that senders MUST omit from `session/chatUpdated` changes + /// and receivers SHOULD ignore. Forks and side chats always use their source /// chat's agent. /// /// Stability: 1.0 - Early development. @@ -2927,9 +2931,15 @@ pub struct SessionConfigState { /// takes its `default` from `schema`. /// /// Settings change only through `chat/configChanged` on the chat, which never -/// changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not -/// valid for the chat's effective schema (`schema` when present, otherwise the -/// session's schema restricted to `chatMutable` properties). +/// changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only +/// `moveChat` normalization can change it. Hosts MUST reject +/// `chat/configChanged` keys that are not valid for the chat's effective +/// schema: `schema`'s `chatMutable` properties when present, otherwise the +/// session's `chatMutable` properties. With `schema`, a `replace` may only +/// change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` +/// value unchanged (hosts accept those keys when their value is unchanged and +/// reject the request otherwise), so those values stay at whatever the host +/// set. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index dc63fbb09..bab3201ca 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -426,14 +426,19 @@ public struct InitializeResult: Codable, Sendable { /// Present when the host lets chats in one session be backed by different /// agents. Clients MAY then pass {@link CreateChatParams.provider} to /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that - /// differs from the session's {@link SessionMetadata.provider}. When absent, - /// every chat uses its session's provider and clients MUST NOT send + /// differs from the session's {@link SessionMetadata.provider}. When absent + /// on a connection, clients on that connection MUST NOT send /// {@link CreateChatParams.provider}. /// /// This is a host capability rather than an {@link AgentCapabilities | agent /// capability} because the host, not an individual agent, decides which /// agents may share a session. Hosts MUST NOT advertise it on connections - /// that negotiated a protocol version earlier than `1.1.0`. + /// that negotiated a protocol version earlier than `1.1.0`. Only a host that + /// advertises it on some connection creates chats whose provider differs from + /// their session's; such chats are shared state, so clients on other + /// connections, including pre-`1.1.0` connections, can observe them. On those + /// connections `provider` and `ChatConfigState.schema` arrive as unknown + /// fields and are ignored, so the chats appear to use the session's agent. /// /// Stability: 1.0 - Early development. public var multipleChatProviders: MultipleChatProvidersCapability? diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 707b78a43..9b72257b9 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1827,11 +1827,15 @@ public struct ChatState: Codable, Sendable { /// Absence means the owning session's /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an /// absent value that way. Hosts MAY omit it when it equals the session's - /// provider and MUST set it when it differs, which only happens when the - /// host advertises {@link InitializeResult.multipleChatProviders}. A chat's - /// agent never changes, including across `moveChat`: like `resource`, it is - /// an identity field that senders MUST omit from `session/chatUpdated` - /// changes and receivers ignore. Forks and side chats always use their source + /// provider and MUST set it when it differs, which only happens on a host + /// that advertises {@link InitializeResult.multipleChatProviders} on some + /// connection. Such a host MUST also set it on a movable chat whose + /// cross-provider moves it accepts (see `moveChat`). Clients that do not + /// understand this field, including clients on pre-`1.1.0` connections, + /// ignore it and show the chat as using the session's agent. A chat's agent + /// never changes, including across `moveChat`: like `resource`, it is an + /// identity field that senders MUST omit from `session/chatUpdated` changes + /// and receivers SHOULD ignore. Forks and side chats always use their source /// chat's agent. /// /// Stability: 1.0 - Early development. diff --git a/docs/.changes/20261020-rust-chat-struct-literals.json b/docs/.changes/20261020-rust-chat-struct-literals.json new file mode 100644 index 000000000..536ec7c0e --- /dev/null +++ b/docs/.changes/20261020-rust-chat-struct-literals.json @@ -0,0 +1,5 @@ +{ + "type": "changed", + "message": "Direct Rust struct literals for `ChatState`, `ChatSummary`, and `SessionChatSummary` must now include `provider: None`, and `ChatConfigState` literals must include `schema: None`.", + "targets": ["rust"] +} diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index c923f5711..4e638d67b 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -106,9 +106,14 @@ session's schema; only its `chatMutable` properties can be set on the chat. In both cases clients change a chat's settings by dispatching [`chat/configChanged`](/reference/chat#chatconfigchangedaction) on that chat, which merges into `values` or replaces them when `replace` is `true`, and never -changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not -valid for the chat's effective schema: `schema` when present, otherwise the -session's schema restricted to `chatMutable` properties. +changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only +[`moveChat` normalization](#validation) can change it. Hosts MUST reject +`chat/configChanged` keys that are not valid for the chat's effective schema: +`schema`'s `chatMutable` properties when present, otherwise the session's +`chatMutable` properties. With `schema`, a `replace` may only change or drop +`chatMutable` keys: it MUST carry every non-`chatMutable` value unchanged +(hosts accept those keys when their value is unchanged and reject the request +otherwise), so those values stay at whatever the host set. ### Chat providers @@ -124,9 +129,11 @@ chat. A host that advertises `ChatState` and on [`SessionChatSummary`](/reference/session#sessionchatsummary)) reports a chat's agent. Absence means the session's `provider`; clients MUST treat it that way. Hosts MAY omit it when it equals the session's provider - and MUST set it when it differs. A chat's agent never changes: `provider` - is an identity field, like `resource`, that senders MUST omit from - `session/chatUpdated` changes and receivers ignore. + and MUST set it when it differs. A host that accepts moving a movable chat + into sessions with a different provider MUST also set it on that chat (see + [Moving chats](#validation)). A chat's agent never changes: `provider` is an + identity field, like `resource`, that senders MUST omit from + `session/chatUpdated` changes and receivers SHOULD ignore. - A client MAY pass [`provider`](/reference/chat#createchatparams) to `createChat` only when the host advertises `multipleChatProviders`. When it is absent, the new chat uses the source chat's agent for forks and side chats, @@ -143,13 +150,17 @@ chat. A host that advertises and agent-default values in `ChatState.config`; see [Per-chat configuration](#per-chat-configuration). - Hosts MUST NOT advertise `multipleChatProviders` on connections that - negotiated a protocol version earlier than `1.1.0`. Hosts that do not - advertise it keep every chat on the session's agent. A client that negotiates - `1.1.0` or later but does not understand `provider` or `config.schema` shows - such chats as using the session's agent and may present the session's - settings for them; hosts still reject `chat/configChanged` keys that are not - valid for the chat's effective schema, so such a client cannot apply - settings the chat's agent does not accept. + negotiated a protocol version earlier than `1.1.0`, and clients on a + connection without it MUST NOT send `provider`. Only a host that advertises + the capability on some connection creates chats whose agent differs from + their session's. Those chats are shared state, so clients on other + connections, including pre-`1.1.0` connections, can observe them; there + `provider` and `config.schema` arrive as unknown fields and are ignored. +- Any client that does not understand `provider` or `config.schema` shows such + chats as using the session's agent and may present the session's settings + for them. Hosts still reject `chat/configChanged` keys that are not valid for + the chat's effective schema, so such a client cannot apply settings the + chat's agent does not accept. ## Relationship to the session channel @@ -291,7 +302,8 @@ destination session; and the source MUST NOT anchor itself. Hosts MAY reject unsupported destinations or transient source conditions. Failure has no observable effect on ownership or catalog order. -Success preserves every moved chat URI, state, and origin, and converges the +Success preserves every moved chat URI, state (apart from the `config` +normalization below), and origin, and converges the durable session catalogs. The protocol does not require provider equality, prohibit active turns universally, or otherwise expose host-specific storage and runtime constraints. @@ -306,21 +318,33 @@ different `provider`. See [Chat providers](#chat-providers). A moved chat always keeps its `provider`. Because no action changes `ChatState.provider`, a host that advertises `multipleChatProviders` MUST set -`provider` on the `ChatState` of every chat it advertises as `movable`. When -ownership changes, such a host MUST normalize each moved chat's -`ChatState.config` for the destination session as part of the committed move: +`provider` on the `ChatState` of a `movable` chat if it accepts moves of that +chat into sessions with a different `provider`; otherwise the normal omission +rule applies (see [Chat providers](#chat-providers)). When ownership changes, +such a host MUST normalize each moved chat's `ChatState.config` for the +destination session as part of the committed move. A value is valid in a schema +when its key is a property of that schema and its value matches the property's +type and any `enum`. - If the chat's agent differs from the destination session's `provider`, `config.schema` is the chat agent's chat-applicable config schema and `config.values` is that agent's defaults overlaid with the chat's existing - values that are valid in that schema. + values that are valid in that schema. Session-level values that are not + `chatMutable` do not carry over. - If they are equal, `config.schema` is removed and `config.values` keeps, as - an overlay, only the existing values whose properties the destination - session's schema declares `chatMutable`. - -No action carries a changed `schema`, so clients subscribed to a moved chat -whose agent relationship to its session changed SHOULD resubscribe to obtain -the normalized `config`. See [Per-chat configuration](#per-chat-configuration). + an overlay, only the existing values that are valid in the destination + session's schema and whose properties it declares `chatMutable`. +- For a `newSession` destination, the new session's `provider` is the + requested chat's agent, its `config` is seeded from that chat's effective + config values, and the chat drops `schema`, keeping as an overlay only its + values that are `chatMutable` in the new session's schema. + +Whenever normalization changes a moved chat's `config.values`, the host MUST +publish `chat/configChanged` with `replace: true` and the normalized values on +that chat's channel. No action carries a changed `schema`, so clients +subscribed to a moved chat whose `config.schema` changed SHOULD resubscribe to +obtain it. Hosts MAY refuse, as an unsupported destination, any move that would +require normalization. See [Per-chat configuration](#per-chat-configuration). #### Atomic synchronization diff --git a/schema/actions.schema.json b/schema/actions.schema.json index c6ff780b2..573fb8043 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5230,7 +5230,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -5492,7 +5492,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index e60864ca9..65ca4c458 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -188,7 +188,7 @@ }, "multipleChatProviders": { "$ref": "#/$defs/MultipleChatProvidersCapability", - "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent,\nevery chat uses its session's provider and clients MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`.\n\nStability: 1.0 - Early development." + "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent\non a connection, clients on that connection MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`. Only a host that\nadvertises it on some connection creates chats whose provider differs from\ntheir session's; such chats are shared state, so clients on other\nconnections, including pre-`1.1.0` connections, can observe them. On those\nconnections `provider` and `ChatConfigState.schema` arrive as unknown\nfields and are ignored, so the chats appear to use the session's agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -1483,7 +1483,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of every chat it advertises as `movable`.\nWhen ownership changes, such a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move:\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values whose\n properties the destination session's schema declares `chatMutable`.\n\nNo action carries a changed `schema`, so clients subscribed to a moved chat\nwhose provider relationship to its session changed SHOULD resubscribe to\nobtain the normalized `config`.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of a `movable` chat if it accepts moves of\nthat chat into sessions with a different provider; otherwise the\n{@link ChatState.provider} omission rule applies. When ownership changes,\nsuch a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move. A value is valid in a schema when its key is a\nproperty of that schema and its value matches the property's type and any\n`enum`.\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema. Session-level values that are not\n `chatMutable` do not carry over.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values that are\n valid in the destination session's schema and whose properties it\n declares `chatMutable`.\n- For a `newSession` destination, the new session's `provider` is the\n requested chat's provider, its `config` is seeded from that chat's\n effective config values, and the chat drops `schema`, keeping as an\n overlay only its values that are `chatMutable` in the new session's\n schema.\n\nWhenever normalization changes a moved chat's `config.values`, the host\nMUST publish `chat/configChanged` with `replace: true` and the normalized\nvalues on that chat's channel. No action carries a changed `schema`, so\nclients subscribed to a moved chat whose `config.schema` changed SHOULD\nresubscribe to obtain it. Hosts MAY refuse, as an unsupported destination,\nany move that would require normalization.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -4515,7 +4515,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -4777,7 +4777,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 0955070c6..6d0daf203 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2640,7 +2640,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2902,7 +2902,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", @@ -6290,7 +6290,7 @@ }, "multipleChatProviders": { "$ref": "#/$defs/MultipleChatProvidersCapability", - "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent,\nevery chat uses its session's provider and clients MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`.\n\nStability: 1.0 - Early development." + "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent\non a connection, clients on that connection MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`. Only a host that\nadvertises it on some connection creates chats whose provider differs from\ntheir session's; such chats are shared state, so clients on other\nconnections, including pre-`1.1.0` connections, can observe them. On those\nconnections `provider` and `ChatConfigState.schema` arrive as unknown\nfields and are ignored, so the chats appear to use the session's agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -7585,7 +7585,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of every chat it advertises as `movable`.\nWhen ownership changes, such a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move:\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values whose\n properties the destination session's schema declares `chatMutable`.\n\nNo action carries a changed `schema`, so clients subscribed to a moved chat\nwhose provider relationship to its session changed SHOULD resubscribe to\nobtain the normalized `config`.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of a `movable` chat if it accepts moves of\nthat chat into sessions with a different provider; otherwise the\n{@link ChatState.provider} omission rule applies. When ownership changes,\nsuch a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move. A value is valid in a schema when its key is a\nproperty of that schema and its value matches the property's type and any\n`enum`.\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema. Session-level values that are not\n `chatMutable` do not carry over.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values that are\n valid in the destination session's schema and whose properties it\n declares `chatMutable`.\n- For a `newSession` destination, the new session's `provider` is the\n requested chat's provider, its `config` is seeded from that chat's\n effective config values, and the chat drops `schema`, keeping as an\n overlay only its values that are `chatMutable` in the new session's\n schema.\n\nWhenever normalization changes a moved chat's `config.values`, the host\nMUST publish `chat/configChanged` with `replace: true` and the normalized\nvalues on that chat's channel. No action carries a changed `schema`, so\nclients subscribed to a moved chat whose `config.schema` changed SHOULD\nresubscribe to obtain it. Hosts MAY refuse, as an unsupported destination,\nany move that would require normalization.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index ec6552b0b..1bfaeb89d 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2942,7 +2942,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -3204,7 +3204,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/state.schema.json b/schema/state.schema.json index 1f08f0b8b..6800bab18 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2551,7 +2551,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens when the\nhost advertises {@link InitializeResult.multipleChatProviders}. A chat's\nagent never changes, including across `moveChat`: like `resource`, it is\nan identity field that senders MUST omit from `session/chatUpdated`\nchanges and receivers ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2813,7 +2813,7 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. Hosts MUST reject `chat/configChanged` keys that are not\nvalid for the chat's effective schema (`schema` when present, otherwise the\nsession's schema restricted to `chatMutable` properties).", + "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 21706eee2..d86cf1546 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -227,22 +227,36 @@ export type ChatMoveDestination = * * A moved chat always keeps its `provider`. Because no action changes * `ChatState.provider`, a host that advertises the capability MUST set - * `provider` on the `ChatState` of every chat it advertises as `movable`. - * When ownership changes, such a host MUST normalize each moved chat's + * `provider` on the `ChatState` of a `movable` chat if it accepts moves of + * that chat into sessions with a different provider; otherwise the + * {@link ChatState.provider} omission rule applies. When ownership changes, + * such a host MUST normalize each moved chat's * {@link ChatConfigState | `ChatState.config`} for the destination session as - * part of the committed move: + * part of the committed move. A value is valid in a schema when its key is a + * property of that schema and its value matches the property's type and any + * `enum`. * * - If the chat's provider differs from the destination session's provider, * `config.schema` is the chat agent's chat-applicable config schema and * `config.values` is that agent's defaults overlaid with the chat's existing - * values that are valid in that schema. + * values that are valid in that schema. Session-level values that are not + * `chatMutable` do not carry over. * - If the providers are equal, `config.schema` is removed and - * `config.values` keeps, as an overlay, only the existing values whose - * properties the destination session's schema declares `chatMutable`. + * `config.values` keeps, as an overlay, only the existing values that are + * valid in the destination session's schema and whose properties it + * declares `chatMutable`. + * - For a `newSession` destination, the new session's `provider` is the + * requested chat's provider, its `config` is seeded from that chat's + * effective config values, and the chat drops `schema`, keeping as an + * overlay only its values that are `chatMutable` in the new session's + * schema. * - * No action carries a changed `schema`, so clients subscribed to a moved chat - * whose provider relationship to its session changed SHOULD resubscribe to - * obtain the normalized `config`. + * Whenever normalization changes a moved chat's `config.values`, the host + * MUST publish `chat/configChanged` with `replace: true` and the normalized + * values on that chat's channel. No action carries a changed `schema`, so + * clients subscribed to a moved chat whose `config.schema` changed SHOULD + * resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, + * any move that would require normalization. * * On success every moved chat keeps its URI, its state (apart from the * `config` normalization above), and its immutable `ChatOrigin`. The host diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 9ce18848f..cbbddc852 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -89,11 +89,15 @@ export interface ChatState { * Absence means the owning session's * {@link SessionMetadata.provider | `provider`}; clients MUST treat an * absent value that way. Hosts MAY omit it when it equals the session's - * provider and MUST set it when it differs, which only happens when the - * host advertises {@link InitializeResult.multipleChatProviders}. A chat's - * agent never changes, including across `moveChat`: like `resource`, it is - * an identity field that senders MUST omit from `session/chatUpdated` - * changes and receivers ignore. Forks and side chats always use their source + * provider and MUST set it when it differs, which only happens on a host + * that advertises {@link InitializeResult.multipleChatProviders} on some + * connection. Such a host MUST also set it on a movable chat whose + * cross-provider moves it accepts (see `moveChat`). Clients that do not + * understand this field, including clients on pre-`1.1.0` connections, + * ignore it and show the chat as using the session's agent. A chat's agent + * never changes, including across `moveChat`: like `resource`, it is an + * identity field that senders MUST omit from `session/chatUpdated` changes + * and receivers SHOULD ignore. Forks and side chats always use their source * chat's agent. * @stability 1.0 */ @@ -460,9 +464,15 @@ export const enum ChatInteractivity { * takes its `default` from `schema`. * * Settings change only through `chat/configChanged` on the chat, which never - * changes `schema`. Hosts MUST reject `chat/configChanged` keys that are not - * valid for the chat's effective schema (`schema` when present, otherwise the - * session's schema restricted to `chatMutable` properties). + * changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only + * `moveChat` normalization can change it. Hosts MUST reject + * `chat/configChanged` keys that are not valid for the chat's effective + * schema: `schema`'s `chatMutable` properties when present, otherwise the + * session's `chatMutable` properties. With `schema`, a `replace` may only + * change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` + * value unchanged (hosts accept those keys when their value is unchanged and + * reject the request otherwise), so those values stay at whatever the host + * set. * * @category Chat Config Types */ diff --git a/types/common/commands.ts b/types/common/commands.ts index 2210961da..53d77b1d1 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -317,14 +317,19 @@ export interface InitializeResult { * Present when the host lets chats in one session be backed by different * agents. Clients MAY then pass {@link CreateChatParams.provider} to * `createChat`, and chats MAY report a {@link ChatSummary.provider} that - * differs from the session's {@link SessionMetadata.provider}. When absent, - * every chat uses its session's provider and clients MUST NOT send + * differs from the session's {@link SessionMetadata.provider}. When absent + * on a connection, clients on that connection MUST NOT send * {@link CreateChatParams.provider}. * * This is a host capability rather than an {@link AgentCapabilities | agent * capability} because the host, not an individual agent, decides which * agents may share a session. Hosts MUST NOT advertise it on connections - * that negotiated a protocol version earlier than `1.1.0`. + * that negotiated a protocol version earlier than `1.1.0`. Only a host that + * advertises it on some connection creates chats whose provider differs from + * their session's; such chats are shared state, so clients on other + * connections, including pre-`1.1.0` connections, can observe them. On those + * connections `provider` and `ChatConfigState.schema` arrive as unknown + * fields and are ignored, so the chats appear to use the session's agent. * @stability 1.0 * @see {@link /specification/chat-channel#chat-providers | Chat Channel: Chat providers} */ diff --git a/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json b/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json new file mode 100644 index 000000000..b7ebc5878 --- /dev/null +++ b/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json @@ -0,0 +1,34 @@ +{ + "description": "chat/configChanged with replace creates the config when it is absent", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "provider": "other-agent" + }, + "actions": [ + { + "type": "chat/configChanged", + "config": { + "autoApprove": true + }, + "replace": true + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "provider": "other-agent", + "config": { + "values": { + "autoApprove": true + } + } + } +} diff --git a/types/test-cases/round-trips/080-session-chat-summary-provider-round-trip.json b/types/test-cases/round-trips/080-session-chat-summary-provider-round-trip.json new file mode 100644 index 000000000..73a0870f9 --- /dev/null +++ b/types/test-cases/round-trips/080-session-chat-summary-provider-round-trip.json @@ -0,0 +1,52 @@ +{ + "name": "session-chat-summary-provider-round-trip", + "group": "A", + "description": "SessionSummary chat catalog entries preserve a per-chat provider so session lists can show sessions whose chats use different agents without subscribing to them.", + "type": "SessionSummary", + "input": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Mixed agents", + "status": 1, + "createdAt": "2026-10-20T10:00:00.000Z", + "modifiedAt": "2026-10-20T10:05:00.000Z", + "chats": [ + { + "resource": "ahp-chat:/main", + "title": "Main", + "status": 1 + }, + { + "resource": "ahp-chat:/other", + "title": "Other agent", + "status": 1, + "provider": "other-agent" + } + ], + "defaultChat": "ahp-chat:/main" + }, + "acceptableOutputs": [ + { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Mixed agents", + "status": 1, + "createdAt": "2026-10-20T10:00:00.000Z", + "modifiedAt": "2026-10-20T10:05:00.000Z", + "chats": [ + { + "resource": "ahp-chat:/main", + "title": "Main", + "status": 1 + }, + { + "resource": "ahp-chat:/other", + "title": "Other agent", + "status": 1, + "provider": "other-agent" + } + ], + "defaultChat": "ahp-chat:/main" + } + ] +} From 0b88b3be19bf5df03567aac096de59f5945dbbb8 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Oct 2026 22:50:01 +0200 Subject: [PATCH 06/13] chat: simplify per-chat provider spec text Same wire types; much less normative text, and no requirements without an implementation. - ChatConfigState.schema is a plain overlay base: when present it replaces the session's schema for the chat and its defaults replace the session's values; `values` stays a chatMutable overlay. Hosts set it when the chat's agent differs. Removes the full-effective-config, seeding, replace-carry, and lifetime rules. - moveChat: drops the normalization block. A moved chat keeps its provider, hosts MAY refuse moves they cannot support, and clients resubscribe when a move changes `config`. - ChatState.provider, the capability doc, CreateChatParams.provider, the Chat providers section, and state-model trimmed to essentials, with the repetition across the four places removed. - Config-schema change fragment and reducer fixture descriptions updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Actions.generated.cs | 12 +- .../Generated/Commands.generated.cs | 111 +++--------- .../Generated/Notifications.generated.cs | 4 +- .../Generated/State.generated.cs | 96 +++-------- clients/go/ahptypes/actions.generated.go | 8 +- clients/go/ahptypes/commands.generated.go | 111 +++--------- .../go/ahptypes/notifications.generated.go | 4 +- clients/go/ahptypes/state.generated.go | 96 +++-------- .../generated/Actions.generated.kt | 8 +- .../generated/Commands.generated.kt | 57 ++----- .../generated/Notifications.generated.kt | 4 +- .../generated/State.generated.kt | 60 ++----- clients/rust/crates/ahp-types/src/actions.rs | 12 +- clients/rust/crates/ahp-types/src/commands.rs | 111 +++--------- .../crates/ahp-types/src/notifications.rs | 4 +- clients/rust/crates/ahp-types/src/state.rs | 96 +++-------- .../Generated/Actions.generated.swift | 8 +- .../Generated/Commands.generated.swift | 57 ++----- .../Generated/Notifications.generated.swift | 4 +- .../Generated/State.generated.swift | 60 ++----- .../.changes/20261019-chat-config-schema.json | 2 +- docs/guide/state-model.md | 24 +-- docs/specification/chat-channel.md | 159 ++++-------------- schema/actions.schema.json | 26 +-- schema/commands.schema.json | 34 ++-- schema/errors.schema.json | 34 ++-- schema/notifications.schema.json | 22 +-- schema/state.schema.json | 20 +-- types/channels-chat/commands.ts | 80 ++------- types/channels-chat/state.ts | 88 +++------- types/channels-session/actions.ts | 8 +- types/channels-session/state.ts | 12 +- types/common/commands.ts | 29 +--- ...t-configchanged-preserves-chat-schema.json | 2 +- ...changed-replace-preserves-chat-schema.json | 2 +- ...ed-replace-creates-config-when-absent.json | 2 +- 36 files changed, 404 insertions(+), 1063 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index 5c90bedf5..b99fd9378 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -1163,8 +1163,8 @@ public sealed record SessionChatRemovedAction /// One existing chat's summary fields changed. /// /// Partial-update semantics: only fields present in `changes` are written; -/// omitted fields are preserved. Identity fields (`resource`, `provider`) MUST -/// NOT be carried in `changes`. No-op when no entry with `chat` exists — clients +/// omitted fields are preserved. Identity fields (`resource`) MUST NOT be +/// carried in `changes`. No-op when no entry with `chat` exists — clients /// SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. /// /// Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -1180,8 +1180,8 @@ public sealed record SessionChatUpdatedAction /// Mutable summary fields that changed; omitted fields are unchanged. /// - /// Identity fields (`resource` and `provider`) never change and MUST be - /// omitted by senders; receivers SHOULD ignore them if present. + /// Identity fields (`resource`, `provider`) never change and MUST be omitted by + /// senders; receivers SHOULD ignore them if present. public required PartialChatSummary Changes { get; init; } } @@ -2781,9 +2781,7 @@ public sealed record PartialChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; init; } - /// Agent provider ID backing this chat. Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}. - /// See {@link ChatState.provider} for the full semantics. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 708b4a967..f949ecec5 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -437,22 +437,15 @@ public sealed record InitializeResult [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public TcpConnectionsCapability? TcpConnections { get; init; } - /// Present when the host lets chats in one session be backed by different - /// agents. Clients MAY then pass {@link CreateChatParams.provider} to - /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that - /// differs from the session's {@link SessionMetadata.provider}. When absent - /// on a connection, clients on that connection MUST NOT send - /// {@link CreateChatParams.provider}. + /// Present when chats in one session may be backed by different agents + /// (see {@link CreateChatParams.provider} and {@link ChatState.provider}). + /// Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not + /// advertise it below protocol `1.1.0`. /// - /// This is a host capability rather than an {@link AgentCapabilities | agent - /// capability} because the host, not an individual agent, decides which - /// agents may share a session. Hosts MUST NOT advertise it on connections - /// that negotiated a protocol version earlier than `1.1.0`. Only a host that - /// advertises it on some connection creates chats whose provider differs from - /// their session's; such chats are shared state, so clients on other - /// connections, including pre-`1.1.0` connections, can observe them. On those - /// connections `provider` and `ChatConfigState.schema` arrive as unknown - /// fields and are ignored, so the chats appear to use the session's agent. + /// Chats are shared state, so a client that does not understand `provider` + /// sees such a chat as using the session's agent and may offer that agent's + /// settings or models for it; hosts reject requests that are invalid for the + /// chat's agent. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -586,11 +579,7 @@ public sealed record AutomationCustomizationsCapability { } -/// Present when the host supports chats backed by different agents within one -/// session. See {@link InitializeResult.multipleChatProviders}. -/// -/// The empty object means "supported"; sub-fields are reserved for future -/// options. +/// Presence capability for {@link InitializeResult.multipleChatProviders}. /// /// Stability: 1.0 - Early development. public sealed record MultipleChatProvidersCapability @@ -999,28 +988,14 @@ public sealed record CreateChatParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Title { get; init; } - /// Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. - /// When absent, the chat uses the source chat's provider if `source` is - /// present, and otherwise the session's - /// {@link SessionMetadata.provider | `provider`}. - /// - /// A client MUST NOT supply this field unless the host advertises - /// {@link InitializeResult.multipleChatProviders}. The host MUST reject the - /// request with `InvalidParams` when: + /// Agent provider ID for the new chat. Absent means the source chat's agent + /// when `source` is present, otherwise the session's provider. /// - /// - it does not advertise `multipleChatProviders`; - /// - the value is not the `provider` of an agent in the root state's - /// `agents`, or that agent does not advertise - /// {@link AgentCapabilities.multipleChats}; or - /// - `source` is present and the value differs from the source chat's - /// provider. Forks and side chats always use their source chat's agent; - /// history is never transferred between agents. - /// - /// Capability checks that depend on the agent — `multipleChats.fork`, - /// `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the - /// chat's agent. The new chat's - /// {@link ChatSummary.provider | `ChatSummary.provider`} reports the - /// resolved agent. + /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + /// `InvalidParams` a provider that is not in the root state's `agents`, whose + /// agent does not advertise `multipleChats`, or that differs from the source + /// chat's agent. Agent capability checks for this request use the chat's + /// agent. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1089,52 +1064,14 @@ public sealed record ChatMoveToNewSessionDestination /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// -/// When the host advertises {@link InitializeResult.multipleChatProviders}, -/// every moved chat keeps its agent ({@link ChatSummary.provider}), and every -/// moved chat whose provider differs from the destination session's -/// {@link SessionMetadata.provider} MUST carry `provider` in its destination -/// catalog entry. A `newSession` destination's session `provider` is the -/// requested chat's provider. A host that does not advertise the capability -/// cannot report a per-chat provider, so it MAY reject, as an unsupported -/// destination, a move that would place a chat in a session with a different -/// provider. -/// -/// A moved chat always keeps its `provider`. Because no action changes -/// `ChatState.provider`, a host that advertises the capability MUST set -/// `provider` on the `ChatState` of a `movable` chat if it accepts moves of -/// that chat into sessions with a different provider; otherwise the -/// {@link ChatState.provider} omission rule applies. When ownership changes, -/// such a host MUST normalize each moved chat's -/// {@link ChatConfigState | `ChatState.config`} for the destination session as -/// part of the committed move. A value is valid in a schema when its key is a -/// property of that schema and its value matches the property's type and any -/// `enum`. -/// -/// - If the chat's provider differs from the destination session's provider, -/// `config.schema` is the chat agent's chat-applicable config schema and -/// `config.values` is that agent's defaults overlaid with the chat's existing -/// values that are valid in that schema. Session-level values that are not -/// `chatMutable` do not carry over. -/// - If the providers are equal, `config.schema` is removed and -/// `config.values` keeps, as an overlay, only the existing values that are -/// valid in the destination session's schema and whose properties it -/// declares `chatMutable`. -/// - For a `newSession` destination, the new session's `provider` is the -/// requested chat's provider, its `config` is seeded from that chat's -/// effective config values, and the chat drops `schema`, keeping as an -/// overlay only its values that are `chatMutable` in the new session's -/// schema. -/// -/// Whenever normalization changes a moved chat's `config.values`, the host -/// MUST publish `chat/configChanged` with `replace: true` and the normalized -/// values on that chat's channel. No action carries a changed `schema`, so -/// clients subscribed to a moved chat whose `config.schema` changed SHOULD -/// resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, -/// any move that would require normalization. -/// -/// On success every moved chat keeps its URI, its state (apart from the -/// `config` normalization above), and its immutable `ChatOrigin`. The host -/// commits ownership and catalog order before publishing +/// A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY +/// refuse, as an unsupported destination, a move they cannot support, such as +/// one that changes whether the chat's agent matches its session's. No action +/// carries a changed `ChatConfigState.schema`, so when a move changes a chat's +/// `config`, clients SHOULD resubscribe to the chat to get it. +/// +/// On success every moved chat keeps its URI, state (except `config`, as +/// above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and /// root summary updates as applicable. Session and root snapshots are the /// durable recovery path after reconnect or an uncertain response. diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs index 3e65eae73..15577ffa8 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs @@ -410,9 +410,7 @@ public sealed record OtlpExportMetricsParams /// Partial equivalent of SessionSummary — every field is optional for delta updates. public sealed record PartialSessionSummary { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Provider { get; init; } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index d68461006..4f61734f7 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2365,9 +2365,7 @@ public sealed class ChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; set; } - /// Agent provider ID backing this chat. Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}. - /// See {@link ChatState.provider} for the full semantics. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2500,21 +2498,11 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; set; } - /// Agent provider ID (an {@link AgentInfo.provider}) backing this chat. - /// - /// Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an - /// absent value that way. Hosts MAY omit it when it equals the session's - /// provider and MUST set it when it differs, which only happens on a host - /// that advertises {@link InitializeResult.multipleChatProviders} on some - /// connection. Such a host MUST also set it on a movable chat whose - /// cross-provider moves it accepts (see `moveChat`). Clients that do not - /// understand this field, including clients on pre-`1.1.0` connections, - /// ignore it and show the chat as using the session's agent. A chat's agent - /// never changes, including across `moveChat`: like `resource`, it is an - /// identity field that senders MUST omit from `session/chatUpdated` changes - /// and receivers SHOULD ignore. Forks and side chats always use their source - /// chat's agent. + /// Agent provider ID backing this chat. Absent means the session's + /// {@link SessionMetadata.provider | `provider`}; hosts set it when it + /// differs. It never changes: like `resource`, it is an identity field and is + /// omitted from `session/chatUpdated` changes. Forks and side chats use the + /// source chat's agent. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2564,9 +2552,8 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Canvases { get; set; } - /// Configuration set on this chat: an overlay on the owning session's - /// {@link SessionState.config | `SessionState.config`}, or the chat's own - /// schema and full values when its agent differs from the session's. See + /// Configuration values set on this chat, applied over the owning session's + /// {@link SessionState.config | `SessionState.config`}. See /// {@link ChatConfigState}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatConfigState? Config { get; set; } @@ -2939,9 +2926,7 @@ public sealed record ChatInputSkipped /// `root/sessionSummaryChanged`. public sealed class SessionState { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID public required string Provider { get; set; } /// Session title @@ -3279,9 +3264,7 @@ public sealed record SessionToolAuthenticationRequest /// carries multiple chats. public sealed class SessionSummary { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID public required string Provider { get; set; } /// Session title @@ -3376,11 +3359,7 @@ public sealed record SessionChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; init; } - /// Agent provider ID backing this chat, matching {@link ChatSummary.provider}. - /// - /// Absence means the session's {@link SessionMetadata.provider | `provider`}. - /// Hosts MUST set it when it differs, so session lists can show sessions - /// whose chats use different agents without subscribing to them. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -3540,54 +3519,29 @@ public sealed class SessionConfigState public required Dictionary Values { get; set; } } -/// Configuration set on a chat. Its meaning depends on whether {@link schema} -/// is present. +/// Configuration values set on a chat: an overlay on the owning session's +/// {@link SessionState.config | `SessionState.config`}. /// -/// - **Without `schema`** (the chat uses the session's agent): {@link values} -/// is an overlay on the owning session's -/// {@link SessionState.config | `SessionState.config`}. The session's config -/// schema applies; the chat's effective config is the session's values with -/// these values applied over them, and only properties with -/// `chatMutable: true` in the session's schema can be set on the chat. A -/// property omitted from `values` takes the session's value. -/// - **With `schema`** (the chat's {@link ChatState.provider | `provider`} -/// differs from the session's): {@link values} is the chat's full effective -/// config and nothing is inherited from the session, whose schema and values -/// belong to the session's agent. The host seeds `values` from the chat -/// agent's defaults when it creates the chat. A property omitted from -/// `values`, including after a `chat/configChanged` with `replace: true`, -/// takes its `default` from `schema`. +/// It carries only the chat's own values; the session's config schema +/// applies. The effective config of a chat is the session's values with +/// these values applied over them. Only properties with `chatMutable: true` +/// in the session's config schema can be set on a chat. /// -/// Settings change only through `chat/configChanged` on the chat, which never -/// changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only -/// `moveChat` normalization can change it. Hosts MUST reject -/// `chat/configChanged` keys that are not valid for the chat's effective -/// schema: `schema`'s `chatMutable` properties when present, otherwise the -/// session's `chatMutable` properties. With `schema`, a `replace` may only -/// change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` -/// value unchanged (hosts accept those keys when their value is unchanged and -/// reject the request otherwise), so those values stay at whatever the host -/// set. +/// When {@link schema} is present, it replaces the session's schema for this +/// chat, and its property `default`s replace the session's values as the base +/// that {@link values} overlays. Hosts reject `chat/configChanged` keys that are +/// not `chatMutable` in the effective schema. public sealed class ChatConfigState { - /// JSON Schema describing the config properties that apply to this chat. - /// - /// The host MUST set it if and only if the chat's - /// {@link ChatState.provider | `provider`} differs from the session's - /// provider, to that agent's chat-applicable config schema, and MUST NOT set - /// it otherwise. When absent, clients MUST use the session's - /// {@link SessionState.config | `SessionState.config.schema`}. When present, - /// clients MUST render and validate this chat's settings against it instead, - /// and only its properties with `chatMutable: true` can be set with - /// `chat/configChanged`. + /// Config schema for this chat. Hosts set it when the chat's agent differs + /// from the session's; when present, clients use it instead of the session's + /// schema. No action changes it. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionConfigSchema? Schema { get; set; } - /// Config values set on this chat, keyed by property id. An overlay on the - /// session's values when {@link schema} is absent; the chat's full effective - /// config when it is present. + /// Config values set on this chat, keyed by property id public required Dictionary Values { get; set; } } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 4d9feebc4..875e45b2b 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -210,8 +210,8 @@ type SessionChatRemovedAction struct { // One existing chat's summary fields changed. // // Partial-update semantics: only fields present in `changes` are written; -// omitted fields are preserved. Identity fields (`resource`, `provider`) MUST -// NOT be carried in `changes`. No-op when no entry with `chat` exists — clients +// omitted fields are preserved. Identity fields (`resource`) MUST NOT be +// carried in `changes`. No-op when no entry with `chat` exists — clients // SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. // // Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -224,8 +224,8 @@ type SessionChatUpdatedAction struct { Chat URI `json:"chat"` // Mutable summary fields that changed; omitted fields are unchanged. // - // Identity fields (`resource` and `provider`) never change and MUST be - // omitted by senders; receivers SHOULD ignore them if present. + // Identity fields (`resource`, `provider`) never change and MUST be omitted by + // senders; receivers SHOULD ignore them if present. Changes PartialChatSummary `json:"changes"` } diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index cc190a332..1caeb617b 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -224,22 +224,15 @@ type InitializeResult struct { // // Stability: 1.0 - Early development. TcpConnections *TcpConnectionsCapability `json:"tcpConnections,omitempty"` - // Present when the host lets chats in one session be backed by different - // agents. Clients MAY then pass {@link CreateChatParams.provider} to - // `createChat`, and chats MAY report a {@link ChatSummary.provider} that - // differs from the session's {@link SessionMetadata.provider}. When absent - // on a connection, clients on that connection MUST NOT send - // {@link CreateChatParams.provider}. + // Present when chats in one session may be backed by different agents + // (see {@link CreateChatParams.provider} and {@link ChatState.provider}). + // Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not + // advertise it below protocol `1.1.0`. // - // This is a host capability rather than an {@link AgentCapabilities | agent - // capability} because the host, not an individual agent, decides which - // agents may share a session. Hosts MUST NOT advertise it on connections - // that negotiated a protocol version earlier than `1.1.0`. Only a host that - // advertises it on some connection creates chats whose provider differs from - // their session's; such chats are shared state, so clients on other - // connections, including pre-`1.1.0` connections, can observe them. On those - // connections `provider` and `ChatConfigState.schema` arrive as unknown - // fields and are ignored, so the chats appear to use the session's agent. + // Chats are shared state, so a client that does not understand `provider` + // sees such a chat as using the session's agent and may offer that agent's + // settings or models for it; hosts reject requests that are invalid for the + // chat's agent. // // Stability: 1.0 - Early development. MultipleChatProviders *MultipleChatProvidersCapability `json:"multipleChatProviders,omitempty"` @@ -326,11 +319,7 @@ type AutomationRunCancellationCapability struct { type AutomationCustomizationsCapability struct { } -// Present when the host supports chats backed by different agents within one -// session. See {@link InitializeResult.multipleChatProviders}. -// -// The empty object means "supported"; sub-fields are reserved for future -// options. +// Presence capability for {@link InitializeResult.multipleChatProviders}. // // Stability: 1.0 - Early development. type MultipleChatProvidersCapability struct { @@ -674,28 +663,14 @@ type CreateChatParams struct { WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Client-selected initial display title for the new chat. Title *string `json:"title,omitempty"` - // Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. - // When absent, the chat uses the source chat's provider if `source` is - // present, and otherwise the session's - // {@link SessionMetadata.provider | `provider`}. - // - // A client MUST NOT supply this field unless the host advertises - // {@link InitializeResult.multipleChatProviders}. The host MUST reject the - // request with `InvalidParams` when: + // Agent provider ID for the new chat. Absent means the source chat's agent + // when `source` is present, otherwise the session's provider. // - // - it does not advertise `multipleChatProviders`; - // - the value is not the `provider` of an agent in the root state's - // `agents`, or that agent does not advertise - // {@link AgentCapabilities.multipleChats}; or - // - `source` is present and the value differs from the source chat's - // provider. Forks and side chats always use their source chat's agent; - // history is never transferred between agents. - // - // Capability checks that depend on the agent — `multipleChats.fork`, - // `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the - // chat's agent. The new chat's - // {@link ChatSummary.provider | `ChatSummary.provider`} reports the - // resolved agent. + // Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + // `InvalidParams` a provider that is not in the root state's `agents`, whose + // agent does not advertise `multipleChats`, or that differs from the source + // chat's agent. Agent capability checks for this request use the chat's + // agent. // // Stability: 1.0 - Early development. Provider *string `json:"provider,omitempty"` @@ -754,52 +729,14 @@ type ChatMoveToNewSessionDestination struct { // Rejection leaves ownership, catalog order, chat state, and root summaries // unchanged. // -// When the host advertises {@link InitializeResult.multipleChatProviders}, -// every moved chat keeps its agent ({@link ChatSummary.provider}), and every -// moved chat whose provider differs from the destination session's -// {@link SessionMetadata.provider} MUST carry `provider` in its destination -// catalog entry. A `newSession` destination's session `provider` is the -// requested chat's provider. A host that does not advertise the capability -// cannot report a per-chat provider, so it MAY reject, as an unsupported -// destination, a move that would place a chat in a session with a different -// provider. -// -// A moved chat always keeps its `provider`. Because no action changes -// `ChatState.provider`, a host that advertises the capability MUST set -// `provider` on the `ChatState` of a `movable` chat if it accepts moves of -// that chat into sessions with a different provider; otherwise the -// {@link ChatState.provider} omission rule applies. When ownership changes, -// such a host MUST normalize each moved chat's -// {@link ChatConfigState | `ChatState.config`} for the destination session as -// part of the committed move. A value is valid in a schema when its key is a -// property of that schema and its value matches the property's type and any -// `enum`. -// -// - If the chat's provider differs from the destination session's provider, -// `config.schema` is the chat agent's chat-applicable config schema and -// `config.values` is that agent's defaults overlaid with the chat's existing -// values that are valid in that schema. Session-level values that are not -// `chatMutable` do not carry over. -// - If the providers are equal, `config.schema` is removed and -// `config.values` keeps, as an overlay, only the existing values that are -// valid in the destination session's schema and whose properties it -// declares `chatMutable`. -// - For a `newSession` destination, the new session's `provider` is the -// requested chat's provider, its `config` is seeded from that chat's -// effective config values, and the chat drops `schema`, keeping as an -// overlay only its values that are `chatMutable` in the new session's -// schema. -// -// Whenever normalization changes a moved chat's `config.values`, the host -// MUST publish `chat/configChanged` with `replace: true` and the normalized -// values on that chat's channel. No action carries a changed `schema`, so -// clients subscribed to a moved chat whose `config.schema` changed SHOULD -// resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, -// any move that would require normalization. -// -// On success every moved chat keeps its URI, its state (apart from the -// `config` normalization above), and its immutable `ChatOrigin`. The host -// commits ownership and catalog order before publishing +// A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY +// refuse, as an unsupported destination, a move they cannot support, such as +// one that changes whether the chat's agent matches its session's. No action +// carries a changed `ChatConfigState.schema`, so when a move changes a chat's +// `config`, clients SHOULD resubscribe to the chat to get it. +// +// On success every moved chat keeps its URI, state (except `config`, as +// above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing // `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and // root summary updates as applicable. Session and root snapshots are the // durable recovery path after reconnect or an uncertain response. diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 858924f93..973e1577c 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -336,9 +336,7 @@ type OtlpExportMetricsParams struct { // PartialSessionSummary is the partial equivalent of SessionSummary — every field is optional for delta updates. type PartialSessionSummary struct { - // Agent provider ID of the session: the agent that backs the session's - // initial chat and every chat that does not report its own - // {@link ChatSummary.provider | `provider`}. + // Agent provider ID Provider *string `json:"provider,omitempty"` // Session title Title *string `json:"title,omitempty"` diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 81c2f96c4..e698d52b1 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -977,9 +977,7 @@ type ConfigSchema struct { // the root channel; the host keeps the two in sync via // `root/sessionSummaryChanged`. type SessionState struct { - // Agent provider ID of the session: the agent that backs the session's - // initial chat and every chat that does not report its own - // {@link ChatSummary.provider | `provider`}. + // Agent provider ID Provider string `json:"provider"` // Session title Title string `json:"title"` @@ -1258,9 +1256,7 @@ type SessionToolAuthenticationRequest struct { // values pass through unchanged). The rules only matter once a session // carries multiple chats. type SessionSummary struct { - // Agent provider ID of the session: the agent that backs the session's - // initial chat and every chat that does not report its own - // {@link ChatSummary.provider | `provider`}. + // Agent provider ID Provider string `json:"provider"` // Session title Title string `json:"title"` @@ -1324,11 +1320,7 @@ type SessionChatSummary struct { // read-only chats. Absence defaults to {@link ChatInteractivity.Full} for // backward compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` - // Agent provider ID backing this chat, matching {@link ChatSummary.provider}. - // - // Absence means the session's {@link SessionMetadata.provider | `provider`}. - // Hosts MUST set it when it differs, so session lists can show sessions - // whose chats use different agents without subscribing to them. + // Agent provider ID backing this chat. See {@link ChatState.provider}. // // Stability: 1.0 - Early development. Provider *string `json:"provider,omitempty"` @@ -1404,21 +1396,11 @@ type ChatState struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` - // Agent provider ID (an {@link AgentInfo.provider}) backing this chat. - // - // Absence means the owning session's - // {@link SessionMetadata.provider | `provider`}; clients MUST treat an - // absent value that way. Hosts MAY omit it when it equals the session's - // provider and MUST set it when it differs, which only happens on a host - // that advertises {@link InitializeResult.multipleChatProviders} on some - // connection. Such a host MUST also set it on a movable chat whose - // cross-provider moves it accepts (see `moveChat`). Clients that do not - // understand this field, including clients on pre-`1.1.0` connections, - // ignore it and show the chat as using the session's agent. A chat's agent - // never changes, including across `moveChat`: like `resource`, it is an - // identity field that senders MUST omit from `session/chatUpdated` changes - // and receivers SHOULD ignore. Forks and side chats always use their source - // chat's agent. + // Agent provider ID backing this chat. Absent means the session's + // {@link SessionMetadata.provider | `provider`}; hosts set it when it + // differs. It never changes: like `resource`, it is an identity field and is + // omitted from `session/chatUpdated` changes. Forks and side chats use the + // source chat's agent. // // Stability: 1.0 - Early development. Provider *string `json:"provider,omitempty"` @@ -1458,9 +1440,8 @@ type ChatState struct { // Clients subscribe to each resource for the experimental presentation // state, including its current live source URL. Canvases []CanvasReference `json:"canvases,omitempty"` - // Configuration set on this chat: an overlay on the owning session's - // {@link SessionState.config | `SessionState.config`}, or the chat's own - // schema and full values when its agent differs from the session's. See + // Configuration values set on this chat, applied over the owning session's + // {@link SessionState.config | `SessionState.config`}. See // {@link ChatConfigState}. Config *ChatConfigState `json:"config,omitempty"` // Completed turns @@ -1566,9 +1547,7 @@ type ChatSummary struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` - // Agent provider ID backing this chat. Absence means the owning session's - // {@link SessionMetadata.provider | `provider`}. - // See {@link ChatState.provider} for the full semantics. + // Agent provider ID backing this chat. See {@link ChatState.provider}. // // Stability: 1.0 - Early development. Provider *string `json:"provider,omitempty"` @@ -1729,51 +1708,26 @@ type SessionConfigState struct { Values map[string]json.RawMessage `json:"values"` } -// Configuration set on a chat. Its meaning depends on whether {@link schema} -// is present. +// Configuration values set on a chat: an overlay on the owning session's +// {@link SessionState.config | `SessionState.config`}. // -// - **Without `schema`** (the chat uses the session's agent): {@link values} -// is an overlay on the owning session's -// {@link SessionState.config | `SessionState.config`}. The session's config -// schema applies; the chat's effective config is the session's values with -// these values applied over them, and only properties with -// `chatMutable: true` in the session's schema can be set on the chat. A -// property omitted from `values` takes the session's value. -// - **With `schema`** (the chat's {@link ChatState.provider | `provider`} -// differs from the session's): {@link values} is the chat's full effective -// config and nothing is inherited from the session, whose schema and values -// belong to the session's agent. The host seeds `values` from the chat -// agent's defaults when it creates the chat. A property omitted from -// `values`, including after a `chat/configChanged` with `replace: true`, -// takes its `default` from `schema`. +// It carries only the chat's own values; the session's config schema +// applies. The effective config of a chat is the session's values with +// these values applied over them. Only properties with `chatMutable: true` +// in the session's config schema can be set on a chat. // -// Settings change only through `chat/configChanged` on the chat, which never -// changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only -// `moveChat` normalization can change it. Hosts MUST reject -// `chat/configChanged` keys that are not valid for the chat's effective -// schema: `schema`'s `chatMutable` properties when present, otherwise the -// session's `chatMutable` properties. With `schema`, a `replace` may only -// change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` -// value unchanged (hosts accept those keys when their value is unchanged and -// reject the request otherwise), so those values stay at whatever the host -// set. +// When {@link schema} is present, it replaces the session's schema for this +// chat, and its property `default`s replace the session's values as the base +// that {@link values} overlays. Hosts reject `chat/configChanged` keys that are +// not `chatMutable` in the effective schema. type ChatConfigState struct { - // JSON Schema describing the config properties that apply to this chat. - // - // The host MUST set it if and only if the chat's - // {@link ChatState.provider | `provider`} differs from the session's - // provider, to that agent's chat-applicable config schema, and MUST NOT set - // it otherwise. When absent, clients MUST use the session's - // {@link SessionState.config | `SessionState.config.schema`}. When present, - // clients MUST render and validate this chat's settings against it instead, - // and only its properties with `chatMutable: true` can be set with - // `chat/configChanged`. + // Config schema for this chat. Hosts set it when the chat's agent differs + // from the session's; when present, clients use it instead of the session's + // schema. No action changes it. // // Stability: 1.0 - Early development. Schema *SessionConfigSchema `json:"schema,omitempty"` - // Config values set on this chat, keyed by property id. An overlay on the - // session's values when {@link schema} is absent; the chat's full effective - // config when it is present. + // Config values set on this chat, keyed by property id Values map[string]json.RawMessage `json:"values"` } 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 365b97e52..5cf311528 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 @@ -231,8 +231,8 @@ data class SessionChatUpdatedAction( /** * Mutable summary fields that changed; omitted fields are unchanged. * - * Identity fields (`resource` and `provider`) never change and MUST be - * omitted by senders; receivers SHOULD ignore them if present. + * Identity fields (`resource`, `provider`) never change and MUST be omitted by + * senders; receivers SHOULD ignore them if present. */ val changes: PartialChatSummary ) @@ -1687,9 +1687,7 @@ data class PartialChatSummary( */ val interactivity: ChatInteractivity? = null, /** - * Agent provider ID backing this chat. Absence means the owning session's - * {@link SessionMetadata.provider | `provider`}. - * See {@link ChatState.provider} for the full semantics. + * Agent provider ID backing this chat. See {@link ChatState.provider}. * * Stability: 1.0 - Early development. */ 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 f32e85380..426db8042 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 @@ -468,22 +468,15 @@ data class InitializeResult( */ val tcpConnections: TcpConnectionsCapability? = null, /** - * Present when the host lets chats in one session be backed by different - * agents. Clients MAY then pass {@link CreateChatParams.provider} to - * `createChat`, and chats MAY report a {@link ChatSummary.provider} that - * differs from the session's {@link SessionMetadata.provider}. When absent - * on a connection, clients on that connection MUST NOT send - * {@link CreateChatParams.provider}. + * Present when chats in one session may be backed by different agents + * (see {@link CreateChatParams.provider} and {@link ChatState.provider}). + * Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not + * advertise it below protocol `1.1.0`. * - * This is a host capability rather than an {@link AgentCapabilities | agent - * capability} because the host, not an individual agent, decides which - * agents may share a session. Hosts MUST NOT advertise it on connections - * that negotiated a protocol version earlier than `1.1.0`. Only a host that - * advertises it on some connection creates chats whose provider differs from - * their session's; such chats are shared state, so clients on other - * connections, including pre-`1.1.0` connections, can observe them. On those - * connections `provider` and `ChatConfigState.schema` arrive as unknown - * fields and are ignored, so the chats appear to use the session's agent. + * Chats are shared state, so a client that does not understand `provider` + * sees such a chat as using the session's agent and may offer that agent's + * settings or models for it; hosts reject requests that are invalid for the + * chat's agent. * * Stability: 1.0 - Early development. */ @@ -556,11 +549,7 @@ class AutomationRunCancellationCapability class AutomationCustomizationsCapability /** - * Present when the host supports chats backed by different agents within one - * session. See {@link InitializeResult.multipleChatProviders}. - * - * The empty object means "supported"; sub-fields are reserved for future - * options. + * Presence capability for {@link InitializeResult.multipleChatProviders}. * * Stability: 1.0 - Early development. */ @@ -939,28 +928,14 @@ data class CreateChatParams( */ val title: String? = null, /** - * Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. - * When absent, the chat uses the source chat's provider if `source` is - * present, and otherwise the session's - * {@link SessionMetadata.provider | `provider`}. - * - * A client MUST NOT supply this field unless the host advertises - * {@link InitializeResult.multipleChatProviders}. The host MUST reject the - * request with `InvalidParams` when: - * - * - it does not advertise `multipleChatProviders`; - * - the value is not the `provider` of an agent in the root state's - * `agents`, or that agent does not advertise - * {@link AgentCapabilities.multipleChats}; or - * - `source` is present and the value differs from the source chat's - * provider. Forks and side chats always use their source chat's agent; - * history is never transferred between agents. + * Agent provider ID for the new chat. Absent means the source chat's agent + * when `source` is present, otherwise the session's provider. * - * Capability checks that depend on the agent — `multipleChats.fork`, - * `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the - * chat's agent. The new chat's - * {@link ChatSummary.provider | `ChatSummary.provider`} reports the - * resolved agent. + * Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + * `InvalidParams` a provider that is not in the root state's `agents`, whose + * agent does not advertise `multipleChats`, or that differs from the source + * chat's agent. Agent capability checks for this request use the chat's + * agent. * * Stability: 1.0 - Early development. */ 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 3e29687ec..761e7513f 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 @@ -356,9 +356,7 @@ data class OtlpExportMetricsParams( @Serializable data class PartialSessionSummary( /** - * Agent provider ID of the session: the agent that backs the session's - * initial chat and every chat that does not report its own - * {@link ChatSummary.provider | `provider`}. + * Agent provider ID */ val provider: String? = 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 06be0d2bb..254bb5e50 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 @@ -1812,21 +1812,11 @@ data class ChatState( */ val interactivity: ChatInteractivity? = null, /** - * Agent provider ID (an {@link AgentInfo.provider}) backing this chat. - * - * Absence means the owning session's - * {@link SessionMetadata.provider | `provider`}; clients MUST treat an - * absent value that way. Hosts MAY omit it when it equals the session's - * provider and MUST set it when it differs, which only happens on a host - * that advertises {@link InitializeResult.multipleChatProviders} on some - * connection. Such a host MUST also set it on a movable chat whose - * cross-provider moves it accepts (see `moveChat`). Clients that do not - * understand this field, including clients on pre-`1.1.0` connections, - * ignore it and show the chat as using the session's agent. A chat's agent - * never changes, including across `moveChat`: like `resource`, it is an - * identity field that senders MUST omit from `session/chatUpdated` changes - * and receivers SHOULD ignore. Forks and side chats always use their source - * chat's agent. + * Agent provider ID backing this chat. Absent means the session's + * {@link SessionMetadata.provider | `provider`}; hosts set it when it + * differs. It never changes: like `resource`, it is an identity field and is + * omitted from `session/chatUpdated` changes. Forks and side chats use the + * source chat's agent. * * Stability: 1.0 - Early development. */ @@ -1876,9 +1866,8 @@ data class ChatState( */ val canvases: List? = null, /** - * Configuration set on this chat: an overlay on the owning session's - * {@link SessionState.config | `SessionState.config`}, or the chat's own - * schema and full values when its agent differs from the session's. See + * Configuration values set on this chat, applied over the owning session's + * {@link SessionState.config | `SessionState.config`}. See * {@link ChatConfigState}. */ val config: ChatConfigState? = null, @@ -1981,9 +1970,7 @@ data class ChatSummary( */ val interactivity: ChatInteractivity? = null, /** - * Agent provider ID backing this chat. Absence means the owning session's - * {@link SessionMetadata.provider | `provider`}. - * See {@link ChatState.provider} for the full semantics. + * Agent provider ID backing this chat. See {@link ChatState.provider}. * * Stability: 1.0 - Early development. */ @@ -2016,9 +2003,7 @@ data class SideChatSelection( @Serializable data class SessionState( /** - * Agent provider ID of the session: the agent that backs the session's - * initial chat and every chat that does not report its own - * {@link ChatSummary.provider | `provider`}. + * Agent provider ID */ val provider: String, /** @@ -2413,9 +2398,7 @@ data class SessionToolAuthenticationRequest( @Serializable data class SessionSummary( /** - * Agent provider ID of the session: the agent that backs the session's - * initial chat and every chat that does not report its own - * {@link ChatSummary.provider | `provider`}. + * Agent provider ID */ val provider: String, /** @@ -2518,11 +2501,7 @@ data class SessionChatSummary( */ val interactivity: ChatInteractivity? = null, /** - * Agent provider ID backing this chat, matching {@link ChatSummary.provider}. - * - * Absence means the session's {@link SessionMetadata.provider | `provider`}. - * Hosts MUST set it when it differs, so session lists can show sessions - * whose chats use different agents without subscribing to them. + * Agent provider ID backing this chat. See {@link ChatState.provider}. * * Stability: 1.0 - Early development. */ @@ -2591,24 +2570,15 @@ data class SessionConfigState( @Serializable data class ChatConfigState( /** - * JSON Schema describing the config properties that apply to this chat. - * - * The host MUST set it if and only if the chat's - * {@link ChatState.provider | `provider`} differs from the session's - * provider, to that agent's chat-applicable config schema, and MUST NOT set - * it otherwise. When absent, clients MUST use the session's - * {@link SessionState.config | `SessionState.config.schema`}. When present, - * clients MUST render and validate this chat's settings against it instead, - * and only its properties with `chatMutable: true` can be set with - * `chat/configChanged`. + * Config schema for this chat. Hosts set it when the chat's agent differs + * from the session's; when present, clients use it instead of the session's + * schema. No action changes it. * * Stability: 1.0 - Early development. */ val schema: SessionConfigSchema? = null, /** - * Config values set on this chat, keyed by property id. An overlay on the - * session's values when {@link schema} is absent; the chat's full effective - * config when it is present. + * Config values set on this chat, keyed by property id */ val values: Map ) diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index f8da218dd..e8601777c 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -543,8 +543,8 @@ pub struct SessionChatRemovedAction { /// One existing chat's summary fields changed. /// /// Partial-update semantics: only fields present in `changes` are written; -/// omitted fields are preserved. Identity fields (`resource`, `provider`) MUST -/// NOT be carried in `changes`. No-op when no entry with `chat` exists — clients +/// omitted fields are preserved. Identity fields (`resource`) MUST NOT be +/// carried in `changes`. No-op when no entry with `chat` exists — clients /// SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. /// /// Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -558,8 +558,8 @@ pub struct SessionChatUpdatedAction { pub chat: Uri, /// Mutable summary fields that changed; omitted fields are unchanged. /// - /// Identity fields (`resource` and `provider`) never change and MUST be - /// omitted by senders; receivers SHOULD ignore them if present. + /// Identity fields (`resource`, `provider`) never change and MUST be omitted by + /// senders; receivers SHOULD ignore them if present. pub changes: PartialChatSummary, } @@ -2395,9 +2395,7 @@ pub struct PartialChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, - /// Agent provider ID backing this chat. Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}. - /// See {@link ChatState.provider} for the full semantics. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 2ce82165a..1e94a6592 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -369,22 +369,15 @@ pub struct InitializeResult { /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] pub tcp_connections: Option, - /// Present when the host lets chats in one session be backed by different - /// agents. Clients MAY then pass {@link CreateChatParams.provider} to - /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that - /// differs from the session's {@link SessionMetadata.provider}. When absent - /// on a connection, clients on that connection MUST NOT send - /// {@link CreateChatParams.provider}. + /// Present when chats in one session may be backed by different agents + /// (see {@link CreateChatParams.provider} and {@link ChatState.provider}). + /// Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not + /// advertise it below protocol `1.1.0`. /// - /// This is a host capability rather than an {@link AgentCapabilities | agent - /// capability} because the host, not an individual agent, decides which - /// agents may share a session. Hosts MUST NOT advertise it on connections - /// that negotiated a protocol version earlier than `1.1.0`. Only a host that - /// advertises it on some connection creates chats whose provider differs from - /// their session's; such chats are shared state, so clients on other - /// connections, including pre-`1.1.0` connections, can observe them. On those - /// connections `provider` and `ChatConfigState.schema` arrive as unknown - /// fields and are ignored, so the chats appear to use the session's agent. + /// Chats are shared state, so a client that does not understand `provider` + /// sees such a chat as using the session's agent and may offer that agent's + /// settings or models for it; hosts reject requests that are invalid for the + /// chat's agent. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -488,11 +481,7 @@ pub struct AutomationRunCancellationCapability {} #[serde(rename_all = "camelCase")] pub struct AutomationCustomizationsCapability {} -/// Present when the host supports chats backed by different agents within one -/// session. See {@link InitializeResult.multipleChatProviders}. -/// -/// The empty object means "supported"; sub-fields are reserved for future -/// options. +/// Presence capability for {@link InitializeResult.multipleChatProviders}. /// /// Stability: 1.0 - Early development. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -930,28 +919,14 @@ pub struct CreateChatParams { /// Client-selected initial display title for the new chat. #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, - /// Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. - /// When absent, the chat uses the source chat's provider if `source` is - /// present, and otherwise the session's - /// {@link SessionMetadata.provider | `provider`}. - /// - /// A client MUST NOT supply this field unless the host advertises - /// {@link InitializeResult.multipleChatProviders}. The host MUST reject the - /// request with `InvalidParams` when: + /// Agent provider ID for the new chat. Absent means the source chat's agent + /// when `source` is present, otherwise the session's provider. /// - /// - it does not advertise `multipleChatProviders`; - /// - the value is not the `provider` of an agent in the root state's - /// `agents`, or that agent does not advertise - /// {@link AgentCapabilities.multipleChats}; or - /// - `source` is present and the value differs from the source chat's - /// provider. Forks and side chats always use their source chat's agent; - /// history is never transferred between agents. - /// - /// Capability checks that depend on the agent — `multipleChats.fork`, - /// `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the - /// chat's agent. The new chat's - /// {@link ChatSummary.provider | `ChatSummary.provider`} reports the - /// resolved agent. + /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + /// `InvalidParams` a provider that is not in the root state's `agents`, whose + /// agent does not advertise `multipleChats`, or that differs from the source + /// chat's agent. Agent capability checks for this request use the chat's + /// agent. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1014,52 +989,14 @@ pub struct ChatMoveToNewSessionDestination {} /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// -/// When the host advertises {@link InitializeResult.multipleChatProviders}, -/// every moved chat keeps its agent ({@link ChatSummary.provider}), and every -/// moved chat whose provider differs from the destination session's -/// {@link SessionMetadata.provider} MUST carry `provider` in its destination -/// catalog entry. A `newSession` destination's session `provider` is the -/// requested chat's provider. A host that does not advertise the capability -/// cannot report a per-chat provider, so it MAY reject, as an unsupported -/// destination, a move that would place a chat in a session with a different -/// provider. -/// -/// A moved chat always keeps its `provider`. Because no action changes -/// `ChatState.provider`, a host that advertises the capability MUST set -/// `provider` on the `ChatState` of a `movable` chat if it accepts moves of -/// that chat into sessions with a different provider; otherwise the -/// {@link ChatState.provider} omission rule applies. When ownership changes, -/// such a host MUST normalize each moved chat's -/// {@link ChatConfigState | `ChatState.config`} for the destination session as -/// part of the committed move. A value is valid in a schema when its key is a -/// property of that schema and its value matches the property's type and any -/// `enum`. -/// -/// - If the chat's provider differs from the destination session's provider, -/// `config.schema` is the chat agent's chat-applicable config schema and -/// `config.values` is that agent's defaults overlaid with the chat's existing -/// values that are valid in that schema. Session-level values that are not -/// `chatMutable` do not carry over. -/// - If the providers are equal, `config.schema` is removed and -/// `config.values` keeps, as an overlay, only the existing values that are -/// valid in the destination session's schema and whose properties it -/// declares `chatMutable`. -/// - For a `newSession` destination, the new session's `provider` is the -/// requested chat's provider, its `config` is seeded from that chat's -/// effective config values, and the chat drops `schema`, keeping as an -/// overlay only its values that are `chatMutable` in the new session's -/// schema. -/// -/// Whenever normalization changes a moved chat's `config.values`, the host -/// MUST publish `chat/configChanged` with `replace: true` and the normalized -/// values on that chat's channel. No action carries a changed `schema`, so -/// clients subscribed to a moved chat whose `config.schema` changed SHOULD -/// resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, -/// any move that would require normalization. -/// -/// On success every moved chat keeps its URI, its state (apart from the -/// `config` normalization above), and its immutable `ChatOrigin`. The host -/// commits ownership and catalog order before publishing +/// A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY +/// refuse, as an unsupported destination, a move they cannot support, such as +/// one that changes whether the chat's agent matches its session's. No action +/// carries a changed `ChatConfigState.schema`, so when a move changes a chat's +/// `config`, clients SHOULD resubscribe to the chat to get it. +/// +/// On success every moved chat keeps its URI, state (except `config`, as +/// above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and /// root summary updates as applicable. Session and root snapshots are the /// durable recovery path after reconnect or an uncertain response. diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index ef0c4d49e..113974cda 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -403,9 +403,7 @@ pub struct OtlpExportMetricsParams { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct PartialSessionSummary { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, /// Session title diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index dbe988945..90ca6b72f 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2063,21 +2063,11 @@ pub struct ChatState { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, - /// Agent provider ID (an {@link AgentInfo.provider}) backing this chat. - /// - /// Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an - /// absent value that way. Hosts MAY omit it when it equals the session's - /// provider and MUST set it when it differs, which only happens on a host - /// that advertises {@link InitializeResult.multipleChatProviders} on some - /// connection. Such a host MUST also set it on a movable chat whose - /// cross-provider moves it accepts (see `moveChat`). Clients that do not - /// understand this field, including clients on pre-`1.1.0` connections, - /// ignore it and show the chat as using the session's agent. A chat's agent - /// never changes, including across `moveChat`: like `resource`, it is an - /// identity field that senders MUST omit from `session/chatUpdated` changes - /// and receivers SHOULD ignore. Forks and side chats always use their source - /// chat's agent. + /// Agent provider ID backing this chat. Absent means the session's + /// {@link SessionMetadata.provider | `provider`}; hosts set it when it + /// differs. It never changes: like `resource`, it is an identity field and is + /// omitted from `session/chatUpdated` changes. Forks and side chats use the + /// source chat's agent. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2122,9 +2112,8 @@ pub struct ChatState { /// state, including its current live source URL. #[serde(default, skip_serializing_if = "Option::is_none")] pub canvases: Option>, - /// Configuration set on this chat: an overlay on the owning session's - /// {@link SessionState.config | `SessionState.config`}, or the chat's own - /// schema and full values when its agent differs from the session's. See + /// Configuration values set on this chat, applied over the owning session's + /// {@link SessionState.config | `SessionState.config`}. See /// {@link ChatConfigState}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, @@ -2252,9 +2241,7 @@ pub struct ChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, - /// Agent provider ID backing this chat. Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}. - /// See {@link ChatState.provider} for the full semantics. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2347,9 +2334,7 @@ pub struct SideChatSelection { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionState { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID pub provider: String, /// Session title pub title: String, @@ -2688,9 +2673,7 @@ pub struct SessionToolAuthenticationRequest { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionSummary { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID pub provider: String, /// Session title pub title: String, @@ -2767,11 +2750,7 @@ pub struct SessionChatSummary { /// backward compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, - /// Agent provider ID backing this chat, matching {@link ChatSummary.provider}. - /// - /// Absence means the session's {@link SessionMetadata.provider | `provider`}. - /// Hosts MUST set it when it differs, so session lists can show sessions - /// whose chats use different agents without subscribing to them. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2912,54 +2891,29 @@ pub struct SessionConfigState { pub values: JsonObject, } -/// Configuration set on a chat. Its meaning depends on whether {@link schema} -/// is present. +/// Configuration values set on a chat: an overlay on the owning session's +/// {@link SessionState.config | `SessionState.config`}. /// -/// - **Without `schema`** (the chat uses the session's agent): {@link values} -/// is an overlay on the owning session's -/// {@link SessionState.config | `SessionState.config`}. The session's config -/// schema applies; the chat's effective config is the session's values with -/// these values applied over them, and only properties with -/// `chatMutable: true` in the session's schema can be set on the chat. A -/// property omitted from `values` takes the session's value. -/// - **With `schema`** (the chat's {@link ChatState.provider | `provider`} -/// differs from the session's): {@link values} is the chat's full effective -/// config and nothing is inherited from the session, whose schema and values -/// belong to the session's agent. The host seeds `values` from the chat -/// agent's defaults when it creates the chat. A property omitted from -/// `values`, including after a `chat/configChanged` with `replace: true`, -/// takes its `default` from `schema`. +/// It carries only the chat's own values; the session's config schema +/// applies. The effective config of a chat is the session's values with +/// these values applied over them. Only properties with `chatMutable: true` +/// in the session's config schema can be set on a chat. /// -/// Settings change only through `chat/configChanged` on the chat, which never -/// changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only -/// `moveChat` normalization can change it. Hosts MUST reject -/// `chat/configChanged` keys that are not valid for the chat's effective -/// schema: `schema`'s `chatMutable` properties when present, otherwise the -/// session's `chatMutable` properties. With `schema`, a `replace` may only -/// change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` -/// value unchanged (hosts accept those keys when their value is unchanged and -/// reject the request otherwise), so those values stay at whatever the host -/// set. +/// When {@link schema} is present, it replaces the session's schema for this +/// chat, and its property `default`s replace the session's values as the base +/// that {@link values} overlays. Hosts reject `chat/configChanged` keys that are +/// not `chatMutable` in the effective schema. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { - /// JSON Schema describing the config properties that apply to this chat. - /// - /// The host MUST set it if and only if the chat's - /// {@link ChatState.provider | `provider`} differs from the session's - /// provider, to that agent's chat-applicable config schema, and MUST NOT set - /// it otherwise. When absent, clients MUST use the session's - /// {@link SessionState.config | `SessionState.config.schema`}. When present, - /// clients MUST render and validate this chat's settings against it instead, - /// and only its properties with `chatMutable: true` can be set with - /// `chat/configChanged`. + /// Config schema for this chat. Hosts set it when the chat's agent differs + /// from the session's; when present, clients use it instead of the session's + /// schema. No action changes it. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, - /// Config values set on this chat, keyed by property id. An overlay on the - /// session's values when {@link schema} is absent; the chat's full effective - /// config when it is present. + /// Config values set on this chat, keyed by property id pub values: JsonObject, } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index b8bab725a..de93675f5 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -472,8 +472,8 @@ public struct SessionChatUpdatedAction: Codable, Sendable { public var chat: String /// Mutable summary fields that changed; omitted fields are unchanged. /// - /// Identity fields (`resource` and `provider`) never change and MUST be - /// omitted by senders; receivers SHOULD ignore them if present. + /// Identity fields (`resource`, `provider`) never change and MUST be omitted by + /// senders; receivers SHOULD ignore them if present. public var changes: PartialChatSummary public init( @@ -2566,9 +2566,7 @@ public struct PartialChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? - /// Agent provider ID backing this chat. Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}. - /// See {@link ChatState.provider} for the full semantics. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. public var provider: String? diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index bab3201ca..04fc8033b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -423,22 +423,15 @@ public struct InitializeResult: Codable, Sendable { /// /// Stability: 1.0 - Early development. public var tcpConnections: TcpConnectionsCapability? - /// Present when the host lets chats in one session be backed by different - /// agents. Clients MAY then pass {@link CreateChatParams.provider} to - /// `createChat`, and chats MAY report a {@link ChatSummary.provider} that - /// differs from the session's {@link SessionMetadata.provider}. When absent - /// on a connection, clients on that connection MUST NOT send - /// {@link CreateChatParams.provider}. + /// Present when chats in one session may be backed by different agents + /// (see {@link CreateChatParams.provider} and {@link ChatState.provider}). + /// Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not + /// advertise it below protocol `1.1.0`. /// - /// This is a host capability rather than an {@link AgentCapabilities | agent - /// capability} because the host, not an individual agent, decides which - /// agents may share a session. Hosts MUST NOT advertise it on connections - /// that negotiated a protocol version earlier than `1.1.0`. Only a host that - /// advertises it on some connection creates chats whose provider differs from - /// their session's; such chats are shared state, so clients on other - /// connections, including pre-`1.1.0` connections, can observe them. On those - /// connections `provider` and `ChatConfigState.schema` arrive as unknown - /// fields and are ignored, so the chats appear to use the session's agent. + /// Chats are shared state, so a client that does not understand `provider` + /// sees such a chat as using the session's agent and may offer that agent's + /// settings or models for it; hosts reject requests that are invalid for the + /// chat's agent. /// /// Stability: 1.0 - Early development. public var multipleChatProviders: MultipleChatProvidersCapability? @@ -576,11 +569,7 @@ public struct AutomationCustomizationsCapability: Codable, Sendable { } } -/// Present when the host supports chats backed by different agents within one -/// session. See {@link InitializeResult.multipleChatProviders}. -/// -/// The empty object means "supported"; sub-fields are reserved for future -/// options. +/// Presence capability for {@link InitializeResult.multipleChatProviders}. /// /// Stability: 1.0 - Early development. public struct MultipleChatProvidersCapability: Codable, Sendable { @@ -1012,28 +1001,14 @@ public struct CreateChatParams: Codable, Sendable { public var workingDirectories: [String]? /// Client-selected initial display title for the new chat. public var title: String? - /// Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. - /// When absent, the chat uses the source chat's provider if `source` is - /// present, and otherwise the session's - /// {@link SessionMetadata.provider | `provider`}. - /// - /// A client MUST NOT supply this field unless the host advertises - /// {@link InitializeResult.multipleChatProviders}. The host MUST reject the - /// request with `InvalidParams` when: - /// - /// - it does not advertise `multipleChatProviders`; - /// - the value is not the `provider` of an agent in the root state's - /// `agents`, or that agent does not advertise - /// {@link AgentCapabilities.multipleChats}; or - /// - `source` is present and the value differs from the source chat's - /// provider. Forks and side chats always use their source chat's agent; - /// history is never transferred between agents. + /// Agent provider ID for the new chat. Absent means the source chat's agent + /// when `source` is present, otherwise the session's provider. /// - /// Capability checks that depend on the agent — `multipleChats.fork`, - /// `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the - /// chat's agent. The new chat's - /// {@link ChatSummary.provider | `ChatSummary.provider`} reports the - /// resolved agent. + /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + /// `InvalidParams` a provider that is not in the root state's `agents`, whose + /// agent does not advertise `multipleChats`, or that differs from the source + /// chat's agent. Agent capability checks for this request use the chat's + /// agent. /// /// Stability: 1.0 - Early development. public var provider: String? diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 444276909..a1155b2e1 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -367,9 +367,7 @@ public struct OtlpExportMetricsParams: Codable, Sendable { // MARK: - Partial Summary Types public struct PartialSessionSummary: Codable, Sendable { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID public var provider: String? /// Session title public var title: String? diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 9b72257b9..086cfddbc 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1822,21 +1822,11 @@ public struct ChatState: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? - /// Agent provider ID (an {@link AgentInfo.provider}) backing this chat. - /// - /// Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}; clients MUST treat an - /// absent value that way. Hosts MAY omit it when it equals the session's - /// provider and MUST set it when it differs, which only happens on a host - /// that advertises {@link InitializeResult.multipleChatProviders} on some - /// connection. Such a host MUST also set it on a movable chat whose - /// cross-provider moves it accepts (see `moveChat`). Clients that do not - /// understand this field, including clients on pre-`1.1.0` connections, - /// ignore it and show the chat as using the session's agent. A chat's agent - /// never changes, including across `moveChat`: like `resource`, it is an - /// identity field that senders MUST omit from `session/chatUpdated` changes - /// and receivers SHOULD ignore. Forks and side chats always use their source - /// chat's agent. + /// Agent provider ID backing this chat. Absent means the session's + /// {@link SessionMetadata.provider | `provider`}; hosts set it when it + /// differs. It never changes: like `resource`, it is an identity field and is + /// omitted from `session/chatUpdated` changes. Forks and side chats use the + /// source chat's agent. /// /// Stability: 1.0 - Early development. public var provider: String? @@ -1876,9 +1866,8 @@ public struct ChatState: Codable, Sendable { /// Clients subscribe to each resource for the experimental presentation /// state, including its current live source URL. public var canvases: [CanvasReference]? - /// Configuration set on this chat: an overlay on the owning session's - /// {@link SessionState.config | `SessionState.config`}, or the chat's own - /// schema and full values when its agent differs from the session's. See + /// Configuration values set on this chat, applied over the owning session's + /// {@link SessionState.config | `SessionState.config`}. See /// {@link ChatConfigState}. public var config: ChatConfigState? /// Completed turns @@ -2018,9 +2007,7 @@ public struct ChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? - /// Agent provider ID backing this chat. Absence means the owning session's - /// {@link SessionMetadata.provider | `provider`}. - /// See {@link ChatState.provider} for the full semantics. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. public var provider: String? @@ -2077,9 +2064,7 @@ public struct SideChatSelection: Codable, Sendable { } public struct SessionState: Codable, Sendable { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID public var provider: String /// Session title public var title: String @@ -2504,9 +2489,7 @@ public struct SessionToolAuthenticationRequest: Codable, Sendable { } public struct SessionSummary: Codable, Sendable { - /// Agent provider ID of the session: the agent that backs the session's - /// initial chat and every chat that does not report its own - /// {@link ChatSummary.provider | `provider`}. + /// Agent provider ID public var provider: String /// Session title public var title: String @@ -2621,11 +2604,7 @@ public struct SessionChatSummary: Codable, Sendable { /// read-only chats. Absence defaults to {@link ChatInteractivity.Full} for /// backward compatibility. public var interactivity: ChatInteractivity? - /// Agent provider ID backing this chat, matching {@link ChatSummary.provider}. - /// - /// Absence means the session's {@link SessionMetadata.provider | `provider`}. - /// Hosts MUST set it when it differs, so session lists can show sessions - /// whose chats use different agents without subscribing to them. + /// Agent provider ID backing this chat. See {@link ChatState.provider}. /// /// Stability: 1.0 - Early development. public var provider: String? @@ -2714,22 +2693,13 @@ public struct SessionConfigState: Codable, Sendable { } public struct ChatConfigState: Codable, Sendable { - /// JSON Schema describing the config properties that apply to this chat. - /// - /// The host MUST set it if and only if the chat's - /// {@link ChatState.provider | `provider`} differs from the session's - /// provider, to that agent's chat-applicable config schema, and MUST NOT set - /// it otherwise. When absent, clients MUST use the session's - /// {@link SessionState.config | `SessionState.config.schema`}. When present, - /// clients MUST render and validate this chat's settings against it instead, - /// and only its properties with `chatMutable: true` can be set with - /// `chat/configChanged`. + /// Config schema for this chat. Hosts set it when the chat's agent differs + /// from the session's; when present, clients use it instead of the session's + /// schema. No action changes it. /// /// Stability: 1.0 - Early development. public var schema: SessionConfigSchema? - /// Config values set on this chat, keyed by property id. An overlay on the - /// session's values when {@link schema} is absent; the chat's full effective - /// config when it is present. + /// Config values set on this chat, keyed by property id public var values: [String: AnyCodable] public init( diff --git a/docs/.changes/20261019-chat-config-schema.json b/docs/.changes/20261019-chat-config-schema.json index 141d81114..10156dc53 100644 --- a/docs/.changes/20261019-chat-config-schema.json +++ b/docs/.changes/20261019-chat-config-schema.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Optional `ChatConfigState.schema` lets a chat carry its own config schema. Hosts set it if and only if the chat's agent differs from its session's; then `values` is the chat's full effective config, seeded from that agent's defaults, with omitted properties taking their schema defaults. Without it, `values` stays an overlay on the session config. `moveChat` normalizes `config` for the destination session." + "message": "Optional `ChatConfigState.schema`: hosts set it when a chat's agent differs from its session's. It replaces the session's schema for that chat, and its property defaults replace the session's values as the base that the chat's `values` overlay." } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 7a68f080a..5420b768a 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -213,24 +213,16 @@ changeset channel contract. Hosts scope a chat's catalogue to its effective working directories: the chat's subset when present, otherwise the full session set. -`config` holds the chat's own settings. Without `config.schema` it is an -overlay on `SessionState.config`: it carries only the values set on this chat, -the session's config schema applies, and the chat's effective config is the -session's values with the chat's values applied over them. Only properties -marked `chatMutable: true` in the session's schema can be set per chat, with -`chat/configChanged`. See +`config` is an overlay on `SessionState.config`: it carries only the values set +on this chat, and the session's config schema applies. A chat's effective +config is the session's values with the chat's values applied over them. Only +properties marked `chatMutable: true` in the session's schema can be set per +chat, with `chat/configChanged`. See [Per-chat configuration](/specification/chat-channel#per-chat-configuration). -`provider` names the chat's agent. Absence means the session's `provider`. -Hosts that advertise `multipleChatProviders` in their `initialize` result may -back chats in one session with different agents, chosen by `createChat`'s -`provider`; forks and side chats keep their source chat's agent, and a chat's -agent never changes. The host sets `config.schema` if and only if a chat's -agent differs from the session's. Such a chat inherits nothing from the -session: `config.values` is its full effective config, seeded from that -agent's defaults, and omitted properties take their `schema` defaults. Clients -render and validate its settings against that schema. `moveChat` normalizes -`config` when a chat's agent relationship to its session changes. See +`provider` names the chat's agent when it differs from the session's. Hosts +advertising `multipleChatProviders` let one session mix agents; such a chat's +`config.schema` replaces the session's schema. See [Chat providers](/specification/chat-channel#chat-providers). Fork and side-chat creation both reference source turns by stable identifiers. diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 4e638d67b..8b546ee4c 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -78,89 +78,39 @@ fully replaces the catalogue or clears it when `changesets` is absent. ### Per-chat configuration -[`ChatState.config`](/reference/chat#chatstate) holds a chat's own settings. -Its meaning depends on whether it carries a `schema`. The host MUST set -`schema` if and only if the chat's agent differs from the session's agent (see -[Chat providers](#chat-providers)), and MUST NOT set it otherwise. - -**Without `schema`**, `config` is an overlay on the owning session's -[`SessionState.config`](/reference/session#sessionstate). It carries only the -values set on this chat; the session's config schema applies. A chat's -effective config is the session's values with the chat's values applied over -them. Hosts do not mirror session config into chats. A host opts a property in -to per-chat values by setting +[`ChatState.config`](/reference/chat#chatstate) is an overlay on the owning +session's [`SessionState.config`](/reference/session#sessionstate). It carries +only the values set on this chat; the session's config schema applies. A +chat's effective config is the session's values with the chat's values +applied over them. Hosts do not mirror session config into chats. + +A host opts a property in to per-chat values by setting [`chatMutable: true`](/reference/session#sessionconfigpropertyschema) on it in -the session's config schema. To reset a property to the session's value, -replace the overlay with one that omits the property. Every other property is -changed on the session with `session/configChanged`. - -**With `schema`**, the chat is backed by a different agent than its session -and inherits nothing from the session, whose schema and values belong to the -session's agent. `schema` is that agent's chat-applicable config schema, and -`values` is the chat's full effective config, seeded from that agent's -defaults when the host creates the chat. A property omitted from `values`, -including after a replace, takes its `default` from `schema`. Clients MUST -render and validate the chat's settings against `schema` rather than the -session's schema; only its `chatMutable` properties can be set on the chat. - -In both cases clients change a chat's settings by dispatching -[`chat/configChanged`](/reference/chat#chatconfigchangedaction) on that chat, -which merges into `values` or replaces them when `replace` is `true`, and never -changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only -[`moveChat` normalization](#validation) can change it. Hosts MUST reject -`chat/configChanged` keys that are not valid for the chat's effective schema: -`schema`'s `chatMutable` properties when present, otherwise the session's -`chatMutable` properties. With `schema`, a `replace` may only change or drop -`chatMutable` keys: it MUST carry every non-`chatMutable` value unchanged -(hosts accept those keys when their value is unchanged and reject the request -otherwise), so those values stay at whatever the host set. +the session's config schema. Clients set such properties for one chat by +dispatching [`chat/configChanged`](/reference/chat#chatconfigchangedaction), +which merges into the chat's overlay or replaces it when `replace` is `true`. +To reset a property to the session's value, replace the overlay with one that +omits the property. Every other property is changed on the session with +`session/configChanged`. + +When a chat's agent differs from the session's, the host also sets +`config.schema`. It replaces the session's schema for that chat, and its +property defaults replace the session's values as the base the overlay applies +to. Hosts reject `chat/configChanged` keys that are not `chatMutable` in the +chat's effective schema. ### Chat providers -Every chat is backed by one agent, identified by its -[`AgentInfo.provider`](/reference/root#agentinfo). -[`SessionState.provider`](/reference/session#sessionmetadata) is the session's -agent: the agent of the session's initial chat and the default for every other -chat. A host that advertises -[`multipleChatProviders`](/reference/common#initializeresult) in its -`initialize` result lets one session contain chats backed by different agents. - -- [`ChatSummary.provider`](/reference/chat#chatsummary) (mirrored on - `ChatState` and on [`SessionChatSummary`](/reference/session#sessionchatsummary)) - reports a chat's agent. Absence means the session's `provider`; clients MUST - treat it that way. Hosts MAY omit it when it equals the session's provider - and MUST set it when it differs. A host that accepts moving a movable chat - into sessions with a different provider MUST also set it on that chat (see - [Moving chats](#validation)). A chat's agent never changes: `provider` is an - identity field, like `resource`, that senders MUST omit from - `session/chatUpdated` changes and receivers SHOULD ignore. -- A client MAY pass [`provider`](/reference/chat#createchatparams) to - `createChat` only when the host advertises `multipleChatProviders`. When it is - absent, the new chat uses the source chat's agent for forks and side chats, - and the session's agent otherwise. -- The host MUST reject `createChat` with `InvalidParams` when `provider` is sent - without the capability, names an agent that is not in the root state's - `agents`, names an agent that does not advertise `multipleChats`, or differs - from the source chat's agent when `source` is present. Forks and side chats - always keep the source chat's agent; history is never transferred between - agents. -- Agent capability checks (`multipleChats.fork`, `multipleChats.sideChat`, - `multipleWorkingDirectories`) use the chat's agent. -- A chat whose agent differs from the session's carries its own config schema - and agent-default values in `ChatState.config`; see - [Per-chat configuration](#per-chat-configuration). -- Hosts MUST NOT advertise `multipleChatProviders` on connections that - negotiated a protocol version earlier than `1.1.0`, and clients on a - connection without it MUST NOT send `provider`. Only a host that advertises - the capability on some connection creates chats whose agent differs from - their session's. Those chats are shared state, so clients on other - connections, including pre-`1.1.0` connections, can observe them; there - `provider` and `config.schema` arrive as unknown fields and are ignored. -- Any client that does not understand `provider` or `config.schema` shows such - chats as using the session's agent and may present the session's settings - for them. Hosts still reject `chat/configChanged` keys that are not valid for - the chat's effective schema, so such a client cannot apply settings the - chat's agent does not accept. +Every chat is backed by one agent. `SessionState.provider` is the session's +agent, and [`ChatSummary.provider`](/reference/chat#chatsummary) names a chat's +agent when it differs; absence means the session's agent. A chat's agent never +changes. + +Hosts that advertise +[`multipleChatProviders`](/reference/common#initializeresult) accept a +`provider` on `createChat`, letting one session mix agents. Forks and side +chats always use the source chat's agent. A client that does not understand +`provider` sees such chats as using the session's agent. ## Relationship to the session channel @@ -183,8 +133,7 @@ chat. A host that advertises [`createChat`](/reference/chat#createchat) is a JSON-RPC request. Callers identify the owning session via the request's `channel` parameter (`ahp-session:/`) and MAY supply: - a `title` containing the client-selected initial display title for the target chat, -- a `provider` selecting the agent that backs the chat, when the host - advertises `multipleChatProviders` (see [Chat providers](#chat-providers)), +- a `provider` selecting the chat's agent (see [Chat providers](#chat-providers)), - an `initialMessage` to start the first turn immediately — carrying its own [`model`](/reference/chat#message) / [`agent`](/reference/chat#message) selection — and - a `source` of type [`ChatSource`](/reference/chat#chatsource), either `{ kind: "fork", chat, turnId }` or `{ kind: "sideChat", chat, turnId }`, @@ -290,8 +239,8 @@ anchor. Hosts publish `chat/movableChanged` and the corresponding ``` The result always reports the authoritative destination session. Every moved -chat keeps its URI, state (apart from the `config` normalization described -under [Validation](#validation)), and byte-for-byte `ChatOrigin`, so existing +chat keeps its URI, state (except `config`; see [Validation](#validation)), and +byte-for-byte `ChatOrigin`, so existing chat-channel subscriptions remain valid. #### Validation @@ -302,49 +251,15 @@ destination session; and the source MUST NOT anchor itself. Hosts MAY reject unsupported destinations or transient source conditions. Failure has no observable effect on ownership or catalog order. -Success preserves every moved chat URI, state (apart from the `config` -normalization below), and origin, and converges the +Success preserves every moved chat URI, state, and origin, and converges the durable session catalogs. The protocol does not require provider equality, prohibit active turns universally, or otherwise expose host-specific storage and runtime constraints. -When the host advertises `multipleChatProviders`, every moved chat keeps its -agent, and a moved chat whose agent differs from the destination session's -`provider` MUST carry `provider` in its destination catalog entry. A -`newSession` destination's `provider` is the requested chat's agent. A host -that does not advertise the capability cannot report a per-chat agent, so it -MAY reject, as an unsupported destination, a move into a session with a -different `provider`. See [Chat providers](#chat-providers). - -A moved chat always keeps its `provider`. Because no action changes -`ChatState.provider`, a host that advertises `multipleChatProviders` MUST set -`provider` on the `ChatState` of a `movable` chat if it accepts moves of that -chat into sessions with a different `provider`; otherwise the normal omission -rule applies (see [Chat providers](#chat-providers)). When ownership changes, -such a host MUST normalize each moved chat's `ChatState.config` for the -destination session as part of the committed move. A value is valid in a schema -when its key is a property of that schema and its value matches the property's -type and any `enum`. - -- If the chat's agent differs from the destination session's `provider`, - `config.schema` is the chat agent's chat-applicable config schema and - `config.values` is that agent's defaults overlaid with the chat's existing - values that are valid in that schema. Session-level values that are not - `chatMutable` do not carry over. -- If they are equal, `config.schema` is removed and `config.values` keeps, as - an overlay, only the existing values that are valid in the destination - session's schema and whose properties it declares `chatMutable`. -- For a `newSession` destination, the new session's `provider` is the - requested chat's agent, its `config` is seeded from that chat's effective - config values, and the chat drops `schema`, keeping as an overlay only its - values that are `chatMutable` in the new session's schema. - -Whenever normalization changes a moved chat's `config.values`, the host MUST -publish `chat/configChanged` with `replace: true` and the normalized values on -that chat's channel. No action carries a changed `schema`, so clients -subscribed to a moved chat whose `config.schema` changed SHOULD resubscribe to -obtain it. Hosts MAY refuse, as an unsupported destination, any move that would -require normalization. See [Per-chat configuration](#per-chat-configuration). +A moved chat keeps its `provider`. Hosts MAY refuse, as an unsupported +destination, a move that changes whether the chat's agent matches its +session's. When a move changes a chat's `config`, clients SHOULD resubscribe to +the chat, since no action carries a changed `config.schema`. #### Atomic synchronization diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 573fb8043..23f3bde66 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -192,7 +192,7 @@ }, "SessionChatUpdatedAction": { "type": "object", - "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`, `provider`) MUST\nNOT be carried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", "properties": { "type": { "const": "session/chatUpdated" @@ -242,7 +242,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -252,7 +252,7 @@ "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` and `provider`) never change and MUST be\nomitted by senders; receivers SHOULD ignore them if present." + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`, `provider`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ @@ -3462,7 +3462,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -3508,7 +3508,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -3816,7 +3816,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -3913,7 +3913,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "status": { "$ref": "#/$defs/SessionStatus", @@ -5230,7 +5230,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -5262,7 +5262,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." + "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -5350,7 +5350,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -5492,16 +5492,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." + "description": "Config values set on this chat, keyed by property id" } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 65ca4c458..ea39047fb 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -188,7 +188,7 @@ }, "multipleChatProviders": { "$ref": "#/$defs/MultipleChatProvidersCapability", - "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent\non a connection, clients on that connection MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`. Only a host that\nadvertises it on some connection creates chats whose provider differs from\ntheir session's; such chats are shared state, so clients on other\nconnections, including pre-`1.1.0` connections, can observe them. On those\nconnections `provider` and `ChatConfigState.schema` arrive as unknown\nfields and are ignored, so the chats appear to use the session's agent.\n\nStability: 1.0 - Early development." + "description": "Present when chats in one session may be backed by different agents\n(see {@link CreateChatParams.provider} and {@link ChatState.provider}).\nClients MUST NOT send `CreateChatParams.provider` without it. Hosts do not\nadvertise it below protocol `1.1.0`.\n\nChats are shared state, so a client that does not understand `provider`\nsees such a chat as using the session's agent and may offer that agent's\nsettings or models for it; hosts reject requests that are invalid for the\nchat's agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -199,7 +199,7 @@ }, "MultipleChatProvidersCapability": { "type": "object", - "description": "Present when the host supports chats backed by different agents within one\nsession. See {@link InitializeResult.multipleChatProviders}.\n\nThe empty object means \"supported\"; sub-fields are reserved for future\noptions.\n\nStability: 1.0 - Early development.", + "description": "Presence capability for {@link InitializeResult.multipleChatProviders}.\n\nStability: 1.0 - Early development.", "properties": {} }, "AutomationCapabilities": { @@ -1438,7 +1438,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat.\nWhen absent, the chat uses the source chat's provider if `source` is\npresent, and otherwise the session's\n{@link SessionMetadata.provider | `provider`}.\n\nA client MUST NOT supply this field unless the host advertises\n{@link InitializeResult.multipleChatProviders}. The host MUST reject the\nrequest with `InvalidParams` when:\n\n- it does not advertise `multipleChatProviders`;\n- the value is not the `provider` of an agent in the root state's\n `agents`, or that agent does not advertise\n {@link AgentCapabilities.multipleChats}; or\n- `source` is present and the value differs from the source chat's\n provider. Forks and side chats always use their source chat's agent;\n history is never transferred between agents.\n\nCapability checks that depend on the agent — `multipleChats.fork`,\n`multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the\nchat's agent. The new chat's\n{@link ChatSummary.provider | `ChatSummary.provider`} reports the\nresolved agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID for the new chat. Absent means the source chat's agent\nwhen `source` is present, otherwise the session's provider.\n\nRequires {@link InitializeResult.multipleChatProviders}. Hosts reject with\n`InvalidParams` a provider that is not in the root state's `agents`, whose\nagent does not advertise `multipleChats`, or that differs from the source\nchat's agent. Agent capability checks for this request use the chat's\nagent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -1483,7 +1483,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of a `movable` chat if it accepts moves of\nthat chat into sessions with a different provider; otherwise the\n{@link ChatState.provider} omission rule applies. When ownership changes,\nsuch a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move. A value is valid in a schema when its key is a\nproperty of that schema and its value matches the property's type and any\n`enum`.\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema. Session-level values that are not\n `chatMutable` do not carry over.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values that are\n valid in the destination session's schema and whose properties it\n declares `chatMutable`.\n- For a `newSession` destination, the new session's `provider` is the\n requested chat's provider, its `config` is seeded from that chat's\n effective config values, and the chat drops `schema`, keeping as an\n overlay only its values that are `chatMutable` in the new session's\n schema.\n\nWhenever normalization changes a moved chat's `config.values`, the host\nMUST publish `chat/configChanged` with `replace: true` and the normalized\nvalues on that chat's channel. No action carries a changed `schema`, so\nclients subscribed to a moved chat whose `config.schema` changed SHOULD\nresubscribe to obtain it. Hosts MAY refuse, as an unsupported destination,\nany move that would require normalization.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY\nrefuse, as an unsupported destination, a move they cannot support, such as\none that changes whether the chat's agent matches its session's. No action\ncarries a changed `ChatConfigState.schema`, so when a move changes a chat's\n`config`, clients SHOULD resubscribe to the chat to get it.\n\nOn success every moved chat keeps its URI, state (except `config`, as\nabove), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -2747,7 +2747,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -2793,7 +2793,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -3101,7 +3101,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -3198,7 +3198,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "status": { "$ref": "#/$defs/SessionStatus", @@ -4515,7 +4515,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -4547,7 +4547,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." + "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -4635,7 +4635,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -4777,16 +4777,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." + "description": "Config values set on this chat, keyed by property id" } }, "required": [ @@ -8169,7 +8169,7 @@ }, "SessionChatUpdatedAction": { "type": "object", - "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`, `provider`) MUST\nNOT be carried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", "properties": { "type": { "const": "session/chatUpdated" @@ -8219,7 +8219,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -8229,7 +8229,7 @@ "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` and `provider`) never change and MUST be\nomitted by senders; receivers SHOULD ignore them if present." + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`, `provider`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 6d0daf203..fb9ba1cc9 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -872,7 +872,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -918,7 +918,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1226,7 +1226,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1323,7 +1323,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "status": { "$ref": "#/$defs/SessionStatus", @@ -2640,7 +2640,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2672,7 +2672,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." + "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -2760,7 +2760,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2902,16 +2902,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." + "description": "Config values set on this chat, keyed by property id" } }, "required": [ @@ -6290,7 +6290,7 @@ }, "multipleChatProviders": { "$ref": "#/$defs/MultipleChatProvidersCapability", - "description": "Present when the host lets chats in one session be backed by different\nagents. Clients MAY then pass {@link CreateChatParams.provider} to\n`createChat`, and chats MAY report a {@link ChatSummary.provider} that\ndiffers from the session's {@link SessionMetadata.provider}. When absent\non a connection, clients on that connection MUST NOT send\n{@link CreateChatParams.provider}.\n\nThis is a host capability rather than an {@link AgentCapabilities | agent\ncapability} because the host, not an individual agent, decides which\nagents may share a session. Hosts MUST NOT advertise it on connections\nthat negotiated a protocol version earlier than `1.1.0`. Only a host that\nadvertises it on some connection creates chats whose provider differs from\ntheir session's; such chats are shared state, so clients on other\nconnections, including pre-`1.1.0` connections, can observe them. On those\nconnections `provider` and `ChatConfigState.schema` arrive as unknown\nfields and are ignored, so the chats appear to use the session's agent.\n\nStability: 1.0 - Early development." + "description": "Present when chats in one session may be backed by different agents\n(see {@link CreateChatParams.provider} and {@link ChatState.provider}).\nClients MUST NOT send `CreateChatParams.provider` without it. Hosts do not\nadvertise it below protocol `1.1.0`.\n\nChats are shared state, so a client that does not understand `provider`\nsees such a chat as using the session's agent and may offer that agent's\nsettings or models for it; hosts reject requests that are invalid for the\nchat's agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -6301,7 +6301,7 @@ }, "MultipleChatProvidersCapability": { "type": "object", - "description": "Present when the host supports chats backed by different agents within one\nsession. See {@link InitializeResult.multipleChatProviders}.\n\nThe empty object means \"supported\"; sub-fields are reserved for future\noptions.\n\nStability: 1.0 - Early development.", + "description": "Presence capability for {@link InitializeResult.multipleChatProviders}.\n\nStability: 1.0 - Early development.", "properties": {} }, "AutomationCapabilities": { @@ -7540,7 +7540,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat.\nWhen absent, the chat uses the source chat's provider if `source` is\npresent, and otherwise the session's\n{@link SessionMetadata.provider | `provider`}.\n\nA client MUST NOT supply this field unless the host advertises\n{@link InitializeResult.multipleChatProviders}. The host MUST reject the\nrequest with `InvalidParams` when:\n\n- it does not advertise `multipleChatProviders`;\n- the value is not the `provider` of an agent in the root state's\n `agents`, or that agent does not advertise\n {@link AgentCapabilities.multipleChats}; or\n- `source` is present and the value differs from the source chat's\n provider. Forks and side chats always use their source chat's agent;\n history is never transferred between agents.\n\nCapability checks that depend on the agent — `multipleChats.fork`,\n`multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the\nchat's agent. The new chat's\n{@link ChatSummary.provider | `ChatSummary.provider`} reports the\nresolved agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID for the new chat. Absent means the source chat's agent\nwhen `source` is present, otherwise the session's provider.\n\nRequires {@link InitializeResult.multipleChatProviders}. Hosts reject with\n`InvalidParams` a provider that is not in the root state's `agents`, whose\nagent does not advertise `multipleChats`, or that differs from the source\nchat's agent. Agent capability checks for this request use the chat's\nagent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -7585,7 +7585,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nWhen the host advertises {@link InitializeResult.multipleChatProviders},\nevery moved chat keeps its agent ({@link ChatSummary.provider}), and every\nmoved chat whose provider differs from the destination session's\n{@link SessionMetadata.provider} MUST carry `provider` in its destination\ncatalog entry. A `newSession` destination's session `provider` is the\nrequested chat's provider. A host that does not advertise the capability\ncannot report a per-chat provider, so it MAY reject, as an unsupported\ndestination, a move that would place a chat in a session with a different\nprovider.\n\nA moved chat always keeps its `provider`. Because no action changes\n`ChatState.provider`, a host that advertises the capability MUST set\n`provider` on the `ChatState` of a `movable` chat if it accepts moves of\nthat chat into sessions with a different provider; otherwise the\n{@link ChatState.provider} omission rule applies. When ownership changes,\nsuch a host MUST normalize each moved chat's\n{@link ChatConfigState | `ChatState.config`} for the destination session as\npart of the committed move. A value is valid in a schema when its key is a\nproperty of that schema and its value matches the property's type and any\n`enum`.\n\n- If the chat's provider differs from the destination session's provider,\n `config.schema` is the chat agent's chat-applicable config schema and\n `config.values` is that agent's defaults overlaid with the chat's existing\n values that are valid in that schema. Session-level values that are not\n `chatMutable` do not carry over.\n- If the providers are equal, `config.schema` is removed and\n `config.values` keeps, as an overlay, only the existing values that are\n valid in the destination session's schema and whose properties it\n declares `chatMutable`.\n- For a `newSession` destination, the new session's `provider` is the\n requested chat's provider, its `config` is seeded from that chat's\n effective config values, and the chat drops `schema`, keeping as an\n overlay only its values that are `chatMutable` in the new session's\n schema.\n\nWhenever normalization changes a moved chat's `config.values`, the host\nMUST publish `chat/configChanged` with `replace: true` and the normalized\nvalues on that chat's channel. No action carries a changed `schema`, so\nclients subscribed to a moved chat whose `config.schema` changed SHOULD\nresubscribe to obtain it. Hosts MAY refuse, as an unsupported destination,\nany move that would require normalization.\n\nOn success every moved chat keeps its URI, its state (apart from the\n`config` normalization above), and its immutable `ChatOrigin`. The host\ncommits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY\nrefuse, as an unsupported destination, a move they cannot support, such as\none that changes whether the chat's agent matches its session's. No action\ncarries a changed `ChatConfigState.schema`, so when a move changes a chat's\n`config`, clients SHOULD resubscribe to the chat to get it.\n\nOn success every moved chat keeps its URI, state (except `config`, as\nabove), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -9359,7 +9359,7 @@ }, "SessionChatUpdatedAction": { "type": "object", - "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`, `provider`) MUST\nNOT be carried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.\nWhen `changes.status` changes, the host MUST project that exact value into\nthe matching `SessionChatSummary.status` field and publish\nthe updated compact chat catalog through `root/sessionSummaryChanged`.", "properties": { "type": { "const": "session/chatUpdated" @@ -9409,7 +9409,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -9419,7 +9419,7 @@ "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` and `provider`) never change and MUST be\nomitted by senders; receivers SHOULD ignore them if present." + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`, `provider`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 1bfaeb89d..b28dd0026 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -141,7 +141,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1174,7 +1174,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1220,7 +1220,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1528,7 +1528,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1625,7 +1625,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "status": { "$ref": "#/$defs/SessionStatus", @@ -2942,7 +2942,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2974,7 +2974,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." + "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -3062,7 +3062,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -3204,16 +3204,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." + "description": "Config values set on this chat, keyed by property id" } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 6800bab18..af66347d2 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -783,7 +783,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -829,7 +829,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1137,7 +1137,7 @@ "properties": { "provider": { "type": "string", - "description": "Agent provider ID of the session: the agent that backs the session's\ninitial chat and every chat that does not report its own\n{@link ChatSummary.provider | `provider`}." + "description": "Agent provider ID" }, "title": { "type": "string", @@ -1234,7 +1234,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat, matching {@link ChatSummary.provider}.\n\nAbsence means the session's {@link SessionMetadata.provider | `provider`}.\nHosts MUST set it when it differs, so session lists can show sessions\nwhose chats use different agents without subscribing to them.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "status": { "$ref": "#/$defs/SessionStatus", @@ -2551,7 +2551,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID (an {@link AgentInfo.provider}) backing this chat.\n\nAbsence means the owning session's\n{@link SessionMetadata.provider | `provider`}; clients MUST treat an\nabsent value that way. Hosts MAY omit it when it equals the session's\nprovider and MUST set it when it differs, which only happens on a host\nthat advertises {@link InitializeResult.multipleChatProviders} on some\nconnection. Such a host MUST also set it on a movable chat whose\ncross-provider moves it accepts (see `moveChat`). Clients that do not\nunderstand this field, including clients on pre-`1.1.0` connections,\nignore it and show the chat as using the session's agent. A chat's agent\nnever changes, including across `moveChat`: like `resource`, it is an\nidentity field that senders MUST omit from `session/chatUpdated` changes\nand receivers SHOULD ignore. Forks and side chats always use their source\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2583,7 +2583,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration set on this chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}, or the chat's own\nschema and full values when its agent differs from the session's. See\n{@link ChatConfigState}." + "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." }, "turns": { "type": "array", @@ -2671,7 +2671,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absence means the owning session's\n{@link SessionMetadata.provider | `provider`}.\nSee {@link ChatState.provider} for the full semantics.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -2813,16 +2813,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration set on a chat. Its meaning depends on whether {@link schema}\nis present.\n\n- **Without `schema`** (the chat uses the session's agent): {@link values}\n is an overlay on the owning session's\n {@link SessionState.config | `SessionState.config`}. The session's config\n schema applies; the chat's effective config is the session's values with\n these values applied over them, and only properties with\n `chatMutable: true` in the session's schema can be set on the chat. A\n property omitted from `values` takes the session's value.\n- **With `schema`** (the chat's {@link ChatState.provider | `provider`}\n differs from the session's): {@link values} is the chat's full effective\n config and nothing is inherited from the session, whose schema and values\n belong to the session's agent. The host seeds `values` from the chat\n agent's defaults when it creates the chat. A property omitted from\n `values`, including after a `chat/configChanged` with `replace: true`,\n takes its `default` from `schema`.\n\nSettings change only through `chat/configChanged` on the chat, which never\nchanges `schema`. A chat's `schema` is fixed for the chat's lifetime; only\n`moveChat` normalization can change it. Hosts MUST reject\n`chat/configChanged` keys that are not valid for the chat's effective\nschema: `schema`'s `chatMutable` properties when present, otherwise the\nsession's `chatMutable` properties. With `schema`, a `replace` may only\nchange or drop `chatMutable` keys: it MUST carry every non-`chatMutable`\nvalue unchanged (hosts accept those keys when their value is unchanged and\nreject the request otherwise), so those values stay at whatever the host\nset.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing the config properties that apply to this chat.\n\nThe host MUST set it if and only if the chat's\n{@link ChatState.provider | `provider`} differs from the session's\nprovider, to that agent's chat-applicable config schema, and MUST NOT set\nit otherwise. When absent, clients MUST use the session's\n{@link SessionState.config | `SessionState.config.schema`}. When present,\nclients MUST render and validate this chat's settings against it instead,\nand only its properties with `chatMutable: true` can be set with\n`chat/configChanged`.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id. An overlay on the\nsession's values when {@link schema} is absent; the chat's full effective\nconfig when it is present." + "description": "Config values set on this chat, keyed by property id" } }, "required": [ diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index d86cf1546..eaea50bf1 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -121,28 +121,14 @@ export interface CreateChatParams extends BaseParams { /** Client-selected initial display title for the new chat. */ title?: string; /** - * Agent provider ID (an {@link AgentInfo.provider}) that backs the new chat. - * When absent, the chat uses the source chat's provider if `source` is - * present, and otherwise the session's - * {@link SessionMetadata.provider | `provider`}. + * Agent provider ID for the new chat. Absent means the source chat's agent + * when `source` is present, otherwise the session's provider. * - * A client MUST NOT supply this field unless the host advertises - * {@link InitializeResult.multipleChatProviders}. The host MUST reject the - * request with `InvalidParams` when: - * - * - it does not advertise `multipleChatProviders`; - * - the value is not the `provider` of an agent in the root state's - * `agents`, or that agent does not advertise - * {@link AgentCapabilities.multipleChats}; or - * - `source` is present and the value differs from the source chat's - * provider. Forks and side chats always use their source chat's agent; - * history is never transferred between agents. - * - * Capability checks that depend on the agent — `multipleChats.fork`, - * `multipleChats.sideChat`, and `multipleWorkingDirectories` — apply to the - * chat's agent. The new chat's - * {@link ChatSummary.provider | `ChatSummary.provider`} reports the - * resolved agent. + * Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + * `InvalidParams` a provider that is not in the root state's `agents`, whose + * agent does not advertise `multipleChats`, or that differs from the source + * chat's agent. Agent capability checks for this request use the chat's + * agent. * @stability 1.0 */ provider?: string; @@ -215,52 +201,14 @@ export type ChatMoveDestination = * Rejection leaves ownership, catalog order, chat state, and root summaries * unchanged. * - * When the host advertises {@link InitializeResult.multipleChatProviders}, - * every moved chat keeps its agent ({@link ChatSummary.provider}), and every - * moved chat whose provider differs from the destination session's - * {@link SessionMetadata.provider} MUST carry `provider` in its destination - * catalog entry. A `newSession` destination's session `provider` is the - * requested chat's provider. A host that does not advertise the capability - * cannot report a per-chat provider, so it MAY reject, as an unsupported - * destination, a move that would place a chat in a session with a different - * provider. - * - * A moved chat always keeps its `provider`. Because no action changes - * `ChatState.provider`, a host that advertises the capability MUST set - * `provider` on the `ChatState` of a `movable` chat if it accepts moves of - * that chat into sessions with a different provider; otherwise the - * {@link ChatState.provider} omission rule applies. When ownership changes, - * such a host MUST normalize each moved chat's - * {@link ChatConfigState | `ChatState.config`} for the destination session as - * part of the committed move. A value is valid in a schema when its key is a - * property of that schema and its value matches the property's type and any - * `enum`. - * - * - If the chat's provider differs from the destination session's provider, - * `config.schema` is the chat agent's chat-applicable config schema and - * `config.values` is that agent's defaults overlaid with the chat's existing - * values that are valid in that schema. Session-level values that are not - * `chatMutable` do not carry over. - * - If the providers are equal, `config.schema` is removed and - * `config.values` keeps, as an overlay, only the existing values that are - * valid in the destination session's schema and whose properties it - * declares `chatMutable`. - * - For a `newSession` destination, the new session's `provider` is the - * requested chat's provider, its `config` is seeded from that chat's - * effective config values, and the chat drops `schema`, keeping as an - * overlay only its values that are `chatMutable` in the new session's - * schema. - * - * Whenever normalization changes a moved chat's `config.values`, the host - * MUST publish `chat/configChanged` with `replace: true` and the normalized - * values on that chat's channel. No action carries a changed `schema`, so - * clients subscribed to a moved chat whose `config.schema` changed SHOULD - * resubscribe to obtain it. Hosts MAY refuse, as an unsupported destination, - * any move that would require normalization. + * A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY + * refuse, as an unsupported destination, a move they cannot support, such as + * one that changes whether the chat's agent matches its session's. No action + * carries a changed `ChatConfigState.schema`, so when a move changes a chat's + * `config`, clients SHOULD resubscribe to the chat to get it. * - * On success every moved chat keeps its URI, its state (apart from the - * `config` normalization above), and its immutable `ChatOrigin`. The host - * commits ownership and catalog order before publishing + * On success every moved chat keeps its URI, state (except `config`, as + * above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing * `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and * root summary updates as applicable. Session and root snapshots are the * durable recovery path after reconnect or an uncertain response. diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index cbbddc852..4fe2e606d 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -84,21 +84,11 @@ export interface ChatState { */ interactivity?: ChatInteractivity; /** - * Agent provider ID (an {@link AgentInfo.provider}) backing this chat. - * - * Absence means the owning session's - * {@link SessionMetadata.provider | `provider`}; clients MUST treat an - * absent value that way. Hosts MAY omit it when it equals the session's - * provider and MUST set it when it differs, which only happens on a host - * that advertises {@link InitializeResult.multipleChatProviders} on some - * connection. Such a host MUST also set it on a movable chat whose - * cross-provider moves it accepts (see `moveChat`). Clients that do not - * understand this field, including clients on pre-`1.1.0` connections, - * ignore it and show the chat as using the session's agent. A chat's agent - * never changes, including across `moveChat`: like `resource`, it is an - * identity field that senders MUST omit from `session/chatUpdated` changes - * and receivers SHOULD ignore. Forks and side chats always use their source - * chat's agent. + * Agent provider ID backing this chat. Absent means the session's + * {@link SessionMetadata.provider | `provider`}; hosts set it when it + * differs. It never changes: like `resource`, it is an identity field and is + * omitted from `session/chatUpdated` changes. Forks and side chats use the + * source chat's agent. * @stability 1.0 */ provider?: string; @@ -147,9 +137,8 @@ export interface ChatState { */ canvases?: CanvasReference[]; /** - * Configuration set on this chat: an overlay on the owning session's - * {@link SessionState.config | `SessionState.config`}, or the chat's own - * schema and full values when its agent differs from the session's. See + * Configuration values set on this chat, applied over the owning session's + * {@link SessionState.config | `SessionState.config`}. See * {@link ChatConfigState}. */ config?: ChatConfigState; @@ -245,9 +234,7 @@ export interface ChatSummary { */ interactivity?: ChatInteractivity; /** - * Agent provider ID backing this chat. Absence means the owning session's - * {@link SessionMetadata.provider | `provider`}. - * See {@link ChatState.provider} for the full semantics. + * Agent provider ID backing this chat. See {@link ChatState.provider}. * @stability 1.0 */ provider?: string; @@ -445,57 +432,30 @@ export const enum ChatInteractivity { // ─── Chat Config Types ─────────────────────────────────────────────────────── /** - * Configuration set on a chat. Its meaning depends on whether {@link schema} - * is present. - * - * - **Without `schema`** (the chat uses the session's agent): {@link values} - * is an overlay on the owning session's - * {@link SessionState.config | `SessionState.config`}. The session's config - * schema applies; the chat's effective config is the session's values with - * these values applied over them, and only properties with - * `chatMutable: true` in the session's schema can be set on the chat. A - * property omitted from `values` takes the session's value. - * - **With `schema`** (the chat's {@link ChatState.provider | `provider`} - * differs from the session's): {@link values} is the chat's full effective - * config and nothing is inherited from the session, whose schema and values - * belong to the session's agent. The host seeds `values` from the chat - * agent's defaults when it creates the chat. A property omitted from - * `values`, including after a `chat/configChanged` with `replace: true`, - * takes its `default` from `schema`. - * - * Settings change only through `chat/configChanged` on the chat, which never - * changes `schema`. A chat's `schema` is fixed for the chat's lifetime; only - * `moveChat` normalization can change it. Hosts MUST reject - * `chat/configChanged` keys that are not valid for the chat's effective - * schema: `schema`'s `chatMutable` properties when present, otherwise the - * session's `chatMutable` properties. With `schema`, a `replace` may only - * change or drop `chatMutable` keys: it MUST carry every non-`chatMutable` - * value unchanged (hosts accept those keys when their value is unchanged and - * reject the request otherwise), so those values stay at whatever the host - * set. + * Configuration values set on a chat: an overlay on the owning session's + * {@link SessionState.config | `SessionState.config`}. + * + * It carries only the chat's own values; the session's config schema + * applies. The effective config of a chat is the session's values with + * these values applied over them. Only properties with `chatMutable: true` + * in the session's config schema can be set on a chat. + * + * When {@link schema} is present, it replaces the session's schema for this + * chat, and its property `default`s replace the session's values as the base + * that {@link values} overlays. Hosts reject `chat/configChanged` keys that are + * not `chatMutable` in the effective schema. * * @category Chat Config Types */ export interface ChatConfigState { /** - * JSON Schema describing the config properties that apply to this chat. - * - * The host MUST set it if and only if the chat's - * {@link ChatState.provider | `provider`} differs from the session's - * provider, to that agent's chat-applicable config schema, and MUST NOT set - * it otherwise. When absent, clients MUST use the session's - * {@link SessionState.config | `SessionState.config.schema`}. When present, - * clients MUST render and validate this chat's settings against it instead, - * and only its properties with `chatMutable: true` can be set with - * `chat/configChanged`. + * Config schema for this chat. Hosts set it when the chat's agent differs + * from the session's; when present, clients use it instead of the session's + * schema. No action changes it. * @stability 1.0 */ schema?: SessionConfigSchema; - /** - * Config values set on this chat, keyed by property id. An overlay on the - * session's values when {@link schema} is absent; the chat's full effective - * config when it is present. - */ + /** Config values set on this chat, keyed by property id */ values: Record; } diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index bf19dcf3c..21098a881 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -76,8 +76,8 @@ export interface SessionChatRemovedAction { * One existing chat's summary fields changed. * * Partial-update semantics: only fields present in `changes` are written; - * omitted fields are preserved. Identity fields (`resource`, `provider`) MUST - * NOT be carried in `changes`. No-op when no entry with `chat` exists — clients + * omitted fields are preserved. Identity fields (`resource`) MUST NOT be + * carried in `changes`. No-op when no entry with `chat` exists — clients * SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. * * Mirrors the root-channel `root/sessionSummaryChanged` notification. @@ -95,8 +95,8 @@ export interface SessionChatUpdatedAction { /** * Mutable summary fields that changed; omitted fields are unchanged. * - * Identity fields (`resource` and `provider`) never change and MUST be - * omitted by senders; receivers SHOULD ignore them if present. + * Identity fields (`resource`, `provider`) never change and MUST be omitted by + * senders; receivers SHOULD ignore them if present. */ changes: Partial; } diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index df512c6ce..87338508d 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -116,11 +116,7 @@ export type SessionOrigin = AutomationSessionOrigin; * @category Session State */ export interface SessionMetadata { - /** - * Agent provider ID of the session: the agent that backs the session's - * initial chat and every chat that does not report its own - * {@link ChatSummary.provider | `provider`}. - */ + /** Agent provider ID */ provider: string; /** Session title */ title: string; @@ -548,11 +544,7 @@ export interface SessionChatSummary { */ interactivity?: ChatInteractivity; /** - * Agent provider ID backing this chat, matching {@link ChatSummary.provider}. - * - * Absence means the session's {@link SessionMetadata.provider | `provider`}. - * Hosts MUST set it when it differs, so session lists can show sessions - * whose chats use different agents without subscribing to them. + * Agent provider ID backing this chat. See {@link ChatState.provider}. * @stability 1.0 */ provider?: string; diff --git a/types/common/commands.ts b/types/common/commands.ts index 53d77b1d1..ae1a82953 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -314,22 +314,15 @@ export interface InitializeResult { */ tcpConnections?: TcpConnectionsCapability; /** - * Present when the host lets chats in one session be backed by different - * agents. Clients MAY then pass {@link CreateChatParams.provider} to - * `createChat`, and chats MAY report a {@link ChatSummary.provider} that - * differs from the session's {@link SessionMetadata.provider}. When absent - * on a connection, clients on that connection MUST NOT send - * {@link CreateChatParams.provider}. + * Present when chats in one session may be backed by different agents + * (see {@link CreateChatParams.provider} and {@link ChatState.provider}). + * Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not + * advertise it below protocol `1.1.0`. * - * This is a host capability rather than an {@link AgentCapabilities | agent - * capability} because the host, not an individual agent, decides which - * agents may share a session. Hosts MUST NOT advertise it on connections - * that negotiated a protocol version earlier than `1.1.0`. Only a host that - * advertises it on some connection creates chats whose provider differs from - * their session's; such chats are shared state, so clients on other - * connections, including pre-`1.1.0` connections, can observe them. On those - * connections `provider` and `ChatConfigState.schema` arrive as unknown - * fields and are ignored, so the chats appear to use the session's agent. + * Chats are shared state, so a client that does not understand `provider` + * sees such a chat as using the session's agent and may offer that agent's + * settings or models for it; hosts reject requests that are invalid for the + * chat's agent. * @stability 1.0 * @see {@link /specification/chat-channel#chat-providers | Chat Channel: Chat providers} */ @@ -337,11 +330,7 @@ export interface InitializeResult { } /** - * Present when the host supports chats backed by different agents within one - * session. See {@link InitializeResult.multipleChatProviders}. - * - * The empty object means "supported"; sub-fields are reserved for future - * options. + * Presence capability for {@link InitializeResult.multipleChatProviders}. * * @category Commands * @stability 1.0 diff --git a/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json b/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json index 32636f445..213d933e5 100644 --- a/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json +++ b/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged merges values and preserves the chat's config schema", + "description": "chat/configChanged merges into the overlay values and preserves the chat's config schema", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json b/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json index c2ad9fa6b..94dcea428 100644 --- a/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json +++ b/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged with replace preserves the chat's config schema", + "description": "chat/configChanged with replace replaces the overlay values and preserves the chat's config schema", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json b/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json index b7ebc5878..e1f0b8502 100644 --- a/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json +++ b/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged with replace creates the config when it is absent", + "description": "chat/configChanged with replace creates the config overlay when it is absent", "reducer": "chat", "initial": { "turns": [], From be8b56b1223f88641a2c95ab9913188b17eff73b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 21 Oct 2026 00:13:12 +0200 Subject: [PATCH 07/13] chat: address review on per-chat provider - Capability doc: just what it allows and that clients need it to send `CreateChatParams.provider`. - CreateChatParams.provider: absent means the session's provider; the source-chat rule applies only to forks and side chats. - moveChat: the host reports `provider` on a moved chat whenever it differs from the destination session's, including a chat that had none. - Drop `ChatConfigState.schema`: a chat on a different agent overlays that agent's config, whose schema clients get from `resolveSessionConfig` with the chat's provider. Removes the schema fixtures, the change fragment, and the Kotlin/Rust reducer changes for it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Commands.generated.cs | 35 +++++----- .../Generated/State.generated.cs | 17 ++--- clients/go/ahptypes/commands.generated.go | 35 +++++----- clients/go/ahptypes/state.generated.go | 15 ++--- .../microsoft/agenthostprotocol/Reducers.kt | 2 +- .../generated/Commands.generated.kt | 20 ++---- .../generated/State.generated.kt | 8 --- clients/rust/crates/ahp-types/src/commands.rs | 35 +++++----- clients/rust/crates/ahp-types/src/state.rs | 16 ++--- clients/rust/crates/ahp/src/reducers.rs | 1 - .../Generated/Commands.generated.swift | 20 ++---- .../Generated/State.generated.swift | 8 --- .../.changes/20261019-chat-config-schema.json | 4 -- ...0261019-kotlin-positional-chat-fields.json | 6 +- .../20261020-rust-chat-struct-literals.json | 6 +- docs/guide/state-model.md | 3 +- docs/specification/chat-channel.md | 25 ++++---- schema/actions.schema.json | 6 +- schema/commands.schema.json | 12 ++-- schema/errors.schema.json | 12 ++-- schema/notifications.schema.json | 6 +- schema/state.schema.json | 6 +- types/channels-chat/commands.ts | 27 ++++---- types/channels-chat/state.ts | 17 ++--- types/common/commands.ts | 8 +-- ...t-configchanged-preserves-chat-schema.json | 60 ----------------- ...changed-replace-preserves-chat-schema.json | 57 ----------------- .../round-trips/056-chat-config-snapshot.json | 2 +- .../079-chat-config-schema-snapshot.json | 64 ------------------- 29 files changed, 128 insertions(+), 405 deletions(-) delete mode 100644 docs/.changes/20261019-chat-config-schema.json delete mode 100644 types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json delete mode 100644 types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json delete mode 100644 types/test-cases/round-trips/079-chat-config-schema-snapshot.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index f949ecec5..a454cd6e9 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -439,13 +439,7 @@ public sealed record InitializeResult /// Present when chats in one session may be backed by different agents /// (see {@link CreateChatParams.provider} and {@link ChatState.provider}). - /// Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not - /// advertise it below protocol `1.1.0`. - /// - /// Chats are shared state, so a client that does not understand `provider` - /// sees such a chat as using the session's agent and may offer that agent's - /// settings or models for it; hosts reject requests that are invalid for the - /// chat's agent. + /// Clients MUST NOT send `CreateChatParams.provider` without it. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -988,14 +982,14 @@ public sealed record CreateChatParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Title { get; init; } - /// Agent provider ID for the new chat. Absent means the source chat's agent - /// when `source` is present, otherwise the session's provider. + /// Agent provider ID for the new chat. Absent means the session's provider. /// /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with - /// `InvalidParams` a provider that is not in the root state's `agents`, whose - /// agent does not advertise `multipleChats`, or that differs from the source - /// chat's agent. Agent capability checks for this request use the chat's - /// agent. + /// `InvalidParams` a provider that is not in the root state's `agents` or + /// whose agent does not advertise `multipleChats`. Forks and side chats + /// (`source` present) continue the source chat's history, so they always use + /// the source chat's agent: absent means that agent, and a different value is + /// rejected. Agent capability checks for this request use the chat's agent. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1064,14 +1058,15 @@ public sealed record ChatMoveToNewSessionDestination /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// -/// A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY -/// refuse, as an unsupported destination, a move they cannot support, such as -/// one that changes whether the chat's agent matches its session's. No action -/// carries a changed `ChatConfigState.schema`, so when a move changes a chat's -/// `config`, clients SHOULD resubscribe to the chat to get it. +/// A moved chat keeps its agent. After the move the host reports +/// {@link ChatState.provider | `provider`} on the chat whenever it differs from +/// the destination session's provider, including when the chat had none +/// because it matched its old session. Hosts MAY refuse, as an unsupported +/// destination, a move they cannot support, such as one that changes whether +/// the chat's agent matches its session's. /// -/// On success every moved chat keeps its URI, state (except `config`, as -/// above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing +/// On success every moved chat keeps its URI, state, and immutable +/// `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and /// root summary updates as applicable. Session and root snapshots are the /// durable recovery path after reconnect or an uncertain response. diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 4f61734f7..aa32506ec 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -3527,20 +3527,13 @@ public sealed class SessionConfigState /// these values applied over them. Only properties with `chatMutable: true` /// in the session's config schema can be set on a chat. /// -/// When {@link schema} is present, it replaces the session's schema for this -/// chat, and its property `default`s replace the session's values as the base -/// that {@link values} overlays. Hosts reject `chat/configChanged` keys that are -/// not `chatMutable` in the effective schema. +/// For a chat whose {@link ChatState.provider | `provider`} differs from the +/// session's, the session's config does not apply: its properties come from +/// that agent's schema, which clients obtain with `resolveSessionConfig` for +/// the chat's `provider`, and `values` overlays that agent's resolved values. +/// Only `chatMutable` properties can be set. public sealed class ChatConfigState { - /// Config schema for this chat. Hosts set it when the chat's agent differs - /// from the session's; when present, clients use it instead of the session's - /// schema. No action changes it. - /// - /// Stability: 1.0 - Early development. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public SessionConfigSchema? Schema { get; set; } - /// Config values set on this chat, keyed by property id public required Dictionary Values { get; set; } } diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 1caeb617b..d94506c0e 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -226,13 +226,7 @@ type InitializeResult struct { TcpConnections *TcpConnectionsCapability `json:"tcpConnections,omitempty"` // Present when chats in one session may be backed by different agents // (see {@link CreateChatParams.provider} and {@link ChatState.provider}). - // Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not - // advertise it below protocol `1.1.0`. - // - // Chats are shared state, so a client that does not understand `provider` - // sees such a chat as using the session's agent and may offer that agent's - // settings or models for it; hosts reject requests that are invalid for the - // chat's agent. + // Clients MUST NOT send `CreateChatParams.provider` without it. // // Stability: 1.0 - Early development. MultipleChatProviders *MultipleChatProvidersCapability `json:"multipleChatProviders,omitempty"` @@ -663,14 +657,14 @@ type CreateChatParams struct { WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Client-selected initial display title for the new chat. Title *string `json:"title,omitempty"` - // Agent provider ID for the new chat. Absent means the source chat's agent - // when `source` is present, otherwise the session's provider. + // Agent provider ID for the new chat. Absent means the session's provider. // // Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with - // `InvalidParams` a provider that is not in the root state's `agents`, whose - // agent does not advertise `multipleChats`, or that differs from the source - // chat's agent. Agent capability checks for this request use the chat's - // agent. + // `InvalidParams` a provider that is not in the root state's `agents` or + // whose agent does not advertise `multipleChats`. Forks and side chats + // (`source` present) continue the source chat's history, so they always use + // the source chat's agent: absent means that agent, and a different value is + // rejected. Agent capability checks for this request use the chat's agent. // // Stability: 1.0 - Early development. Provider *string `json:"provider,omitempty"` @@ -729,14 +723,15 @@ type ChatMoveToNewSessionDestination struct { // Rejection leaves ownership, catalog order, chat state, and root summaries // unchanged. // -// A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY -// refuse, as an unsupported destination, a move they cannot support, such as -// one that changes whether the chat's agent matches its session's. No action -// carries a changed `ChatConfigState.schema`, so when a move changes a chat's -// `config`, clients SHOULD resubscribe to the chat to get it. +// A moved chat keeps its agent. After the move the host reports +// {@link ChatState.provider | `provider`} on the chat whenever it differs from +// the destination session's provider, including when the chat had none +// because it matched its old session. Hosts MAY refuse, as an unsupported +// destination, a move they cannot support, such as one that changes whether +// the chat's agent matches its session's. // -// On success every moved chat keeps its URI, state (except `config`, as -// above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing +// On success every moved chat keeps its URI, state, and immutable +// `ChatOrigin`. The host commits ownership and catalog order before publishing // `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and // root summary updates as applicable. Session and root snapshots are the // durable recovery path after reconnect or an uncertain response. diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index e698d52b1..c8f5d8f39 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1716,17 +1716,12 @@ type SessionConfigState struct { // these values applied over them. Only properties with `chatMutable: true` // in the session's config schema can be set on a chat. // -// When {@link schema} is present, it replaces the session's schema for this -// chat, and its property `default`s replace the session's values as the base -// that {@link values} overlays. Hosts reject `chat/configChanged` keys that are -// not `chatMutable` in the effective schema. +// For a chat whose {@link ChatState.provider | `provider`} differs from the +// session's, the session's config does not apply: its properties come from +// that agent's schema, which clients obtain with `resolveSessionConfig` for +// the chat's `provider`, and `values` overlays that agent's resolved values. +// Only `chatMutable` properties can be set. type ChatConfigState struct { - // Config schema for this chat. Hosts set it when the chat's agent differs - // from the session's; when present, clients use it instead of the session's - // schema. No action changes it. - // - // Stability: 1.0 - Early development. - Schema *SessionConfigSchema `json:"schema,omitempty"` // Config values set on this chat, keyed by property id Values map[string]json.RawMessage `json:"values"` } 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 04f6335b1..6aaa44802 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -1634,7 +1634,7 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when val a = action.value val current = state.config val newValues = if (a.replace == true || current == null) a.config else current.values + a.config - state.copy(config = current?.copy(values = newValues) ?: ChatConfigState(values = newValues)) + state.copy(config = ChatConfigState(values = newValues)) } else -> state 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 426db8042..d9a014f9a 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 @@ -470,13 +470,7 @@ data class InitializeResult( /** * Present when chats in one session may be backed by different agents * (see {@link CreateChatParams.provider} and {@link ChatState.provider}). - * Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not - * advertise it below protocol `1.1.0`. - * - * Chats are shared state, so a client that does not understand `provider` - * sees such a chat as using the session's agent and may offer that agent's - * settings or models for it; hosts reject requests that are invalid for the - * chat's agent. + * Clients MUST NOT send `CreateChatParams.provider` without it. * * Stability: 1.0 - Early development. */ @@ -928,14 +922,14 @@ data class CreateChatParams( */ val title: String? = null, /** - * Agent provider ID for the new chat. Absent means the source chat's agent - * when `source` is present, otherwise the session's provider. + * Agent provider ID for the new chat. Absent means the session's provider. * * Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with - * `InvalidParams` a provider that is not in the root state's `agents`, whose - * agent does not advertise `multipleChats`, or that differs from the source - * chat's agent. Agent capability checks for this request use the chat's - * agent. + * `InvalidParams` a provider that is not in the root state's `agents` or + * whose agent does not advertise `multipleChats`. Forks and side chats + * (`source` present) continue the source chat's history, so they always use + * the source chat's agent: absent means that agent, and a different value is + * rejected. Agent capability checks for this request use the chat's agent. * * Stability: 1.0 - Early development. */ 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 254bb5e50..4c88ff8b4 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 @@ -2569,14 +2569,6 @@ data class SessionConfigState( @Serializable data class ChatConfigState( - /** - * Config schema for this chat. Hosts set it when the chat's agent differs - * from the session's; when present, clients use it instead of the session's - * schema. No action changes it. - * - * Stability: 1.0 - Early development. - */ - val schema: SessionConfigSchema? = null, /** * Config values set on this chat, keyed by property id */ diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 1e94a6592..9cdaa6935 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -371,13 +371,7 @@ pub struct InitializeResult { pub tcp_connections: Option, /// Present when chats in one session may be backed by different agents /// (see {@link CreateChatParams.provider} and {@link ChatState.provider}). - /// Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not - /// advertise it below protocol `1.1.0`. - /// - /// Chats are shared state, so a client that does not understand `provider` - /// sees such a chat as using the session's agent and may offer that agent's - /// settings or models for it; hosts reject requests that are invalid for the - /// chat's agent. + /// Clients MUST NOT send `CreateChatParams.provider` without it. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -919,14 +913,14 @@ pub struct CreateChatParams { /// Client-selected initial display title for the new chat. #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, - /// Agent provider ID for the new chat. Absent means the source chat's agent - /// when `source` is present, otherwise the session's provider. + /// Agent provider ID for the new chat. Absent means the session's provider. /// /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with - /// `InvalidParams` a provider that is not in the root state's `agents`, whose - /// agent does not advertise `multipleChats`, or that differs from the source - /// chat's agent. Agent capability checks for this request use the chat's - /// agent. + /// `InvalidParams` a provider that is not in the root state's `agents` or + /// whose agent does not advertise `multipleChats`. Forks and side chats + /// (`source` present) continue the source chat's history, so they always use + /// the source chat's agent: absent means that agent, and a different value is + /// rejected. Agent capability checks for this request use the chat's agent. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -989,14 +983,15 @@ pub struct ChatMoveToNewSessionDestination {} /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// -/// A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY -/// refuse, as an unsupported destination, a move they cannot support, such as -/// one that changes whether the chat's agent matches its session's. No action -/// carries a changed `ChatConfigState.schema`, so when a move changes a chat's -/// `config`, clients SHOULD resubscribe to the chat to get it. +/// A moved chat keeps its agent. After the move the host reports +/// {@link ChatState.provider | `provider`} on the chat whenever it differs from +/// the destination session's provider, including when the chat had none +/// because it matched its old session. Hosts MAY refuse, as an unsupported +/// destination, a move they cannot support, such as one that changes whether +/// the chat's agent matches its session's. /// -/// On success every moved chat keeps its URI, state (except `config`, as -/// above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing +/// On success every moved chat keeps its URI, state, and immutable +/// `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and /// root summary updates as applicable. Session and root snapshots are the /// durable recovery path after reconnect or an uncertain response. diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 90ca6b72f..9ac7a5c93 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2899,20 +2899,14 @@ pub struct SessionConfigState { /// these values applied over them. Only properties with `chatMutable: true` /// in the session's config schema can be set on a chat. /// -/// When {@link schema} is present, it replaces the session's schema for this -/// chat, and its property `default`s replace the session's values as the base -/// that {@link values} overlays. Hosts reject `chat/configChanged` keys that are -/// not `chatMutable` in the effective schema. +/// For a chat whose {@link ChatState.provider | `provider`} differs from the +/// session's, the session's config does not apply: its properties come from +/// that agent's schema, which clients obtain with `resolveSessionConfig` for +/// the chat's `provider`, and `values` overlays that agent's resolved values. +/// Only `chatMutable` properties can be set. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { - /// Config schema for this chat. Hosts set it when the chat's agent differs - /// from the session's; when present, clients use it instead of the session's - /// schema. No action changes it. - /// - /// Stability: 1.0 - Early development. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema: Option, /// Config values set on this chat, keyed by property id pub values: JsonObject, } diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 79cae8a8f..c4a63b1f3 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -1421,7 +1421,6 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu } StateAction::ChatConfigChanged(a) => { let config = state.config.get_or_insert_with(|| ChatConfigState { - schema: None, values: Default::default(), }); if a.replace.unwrap_or(false) { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 04fc8033b..e91eeb279 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -425,13 +425,7 @@ public struct InitializeResult: Codable, Sendable { public var tcpConnections: TcpConnectionsCapability? /// Present when chats in one session may be backed by different agents /// (see {@link CreateChatParams.provider} and {@link ChatState.provider}). - /// Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not - /// advertise it below protocol `1.1.0`. - /// - /// Chats are shared state, so a client that does not understand `provider` - /// sees such a chat as using the session's agent and may offer that agent's - /// settings or models for it; hosts reject requests that are invalid for the - /// chat's agent. + /// Clients MUST NOT send `CreateChatParams.provider` without it. /// /// Stability: 1.0 - Early development. public var multipleChatProviders: MultipleChatProvidersCapability? @@ -1001,14 +995,14 @@ public struct CreateChatParams: Codable, Sendable { public var workingDirectories: [String]? /// Client-selected initial display title for the new chat. public var title: String? - /// Agent provider ID for the new chat. Absent means the source chat's agent - /// when `source` is present, otherwise the session's provider. + /// Agent provider ID for the new chat. Absent means the session's provider. /// /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with - /// `InvalidParams` a provider that is not in the root state's `agents`, whose - /// agent does not advertise `multipleChats`, or that differs from the source - /// chat's agent. Agent capability checks for this request use the chat's - /// agent. + /// `InvalidParams` a provider that is not in the root state's `agents` or + /// whose agent does not advertise `multipleChats`. Forks and side chats + /// (`source` present) continue the source chat's history, so they always use + /// the source chat's agent: absent means that agent, and a different value is + /// rejected. Agent capability checks for this request use the chat's agent. /// /// Stability: 1.0 - Early development. public var provider: String? diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 086cfddbc..4799a30e2 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -2693,20 +2693,12 @@ public struct SessionConfigState: Codable, Sendable { } public struct ChatConfigState: Codable, Sendable { - /// Config schema for this chat. Hosts set it when the chat's agent differs - /// from the session's; when present, clients use it instead of the session's - /// schema. No action changes it. - /// - /// Stability: 1.0 - Early development. - public var schema: SessionConfigSchema? /// Config values set on this chat, keyed by property id public var values: [String: AnyCodable] public init( - schema: SessionConfigSchema? = nil, values: [String: AnyCodable] ) { - self.schema = schema self.values = values } } diff --git a/docs/.changes/20261019-chat-config-schema.json b/docs/.changes/20261019-chat-config-schema.json deleted file mode 100644 index 10156dc53..000000000 --- a/docs/.changes/20261019-chat-config-schema.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "added", - "message": "Optional `ChatConfigState.schema`: hosts set it when a chat's agent differs from its session's. It replaces the session's schema for that chat, and its property defaults replace the session's values as the base that the chat's `values` overlay." -} diff --git a/docs/.changes/20261019-kotlin-positional-chat-fields.json b/docs/.changes/20261019-kotlin-positional-chat-fields.json index c5327f166..126588623 100644 --- a/docs/.changes/20261019-kotlin-positional-chat-fields.json +++ b/docs/.changes/20261019-kotlin-positional-chat-fields.json @@ -1,5 +1,7 @@ { "type": "changed", - "message": "`ChatState`, `ChatSummary`, `SessionChatSummary`, and `ChatConfigState` gained parameters (`provider`, `schema`), which shifts their positional constructor arguments and `componentN` destructuring; use named arguments.", - "targets": ["kotlin"] + "message": "`ChatState`, `ChatSummary`, and `SessionChatSummary` gained a `provider` parameter, which shifts their positional constructor arguments and `componentN` destructuring; use named arguments.", + "targets": [ + "kotlin" + ] } diff --git a/docs/.changes/20261020-rust-chat-struct-literals.json b/docs/.changes/20261020-rust-chat-struct-literals.json index 536ec7c0e..b240aff09 100644 --- a/docs/.changes/20261020-rust-chat-struct-literals.json +++ b/docs/.changes/20261020-rust-chat-struct-literals.json @@ -1,5 +1,7 @@ { "type": "changed", - "message": "Direct Rust struct literals for `ChatState`, `ChatSummary`, and `SessionChatSummary` must now include `provider: None`, and `ChatConfigState` literals must include `schema: None`.", - "targets": ["rust"] + "message": "Direct Rust struct literals for `ChatState`, `ChatSummary`, and `SessionChatSummary` must now include `provider: None`.", + "targets": [ + "rust" + ] } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 5420b768a..7a546a4e0 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -221,8 +221,7 @@ chat, with `chat/configChanged`. See [Per-chat configuration](/specification/chat-channel#per-chat-configuration). `provider` names the chat's agent when it differs from the session's. Hosts -advertising `multipleChatProviders` let one session mix agents; such a chat's -`config.schema` replaces the session's schema. See +advertising `multipleChatProviders` let one session mix agents. See [Chat providers](/specification/chat-channel#chat-providers). Fork and side-chat creation both reference source turns by stable identifiers. diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 8b546ee4c..b49c671d4 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -93,11 +93,11 @@ To reset a property to the session's value, replace the overlay with one that omits the property. Every other property is changed on the session with `session/configChanged`. -When a chat's agent differs from the session's, the host also sets -`config.schema`. It replaces the session's schema for that chat, and its -property defaults replace the session's values as the base the overlay applies -to. Hosts reject `chat/configChanged` keys that are not `chatMutable` in the -chat's effective schema. +For a chat whose `provider` differs from the session's, the session's config +does not apply: its properties come from that agent's schema, which clients +obtain with [`resolveSessionConfig`](/reference/root#resolvesessionconfig) for +the chat's `provider`, and `values` overlays that agent's resolved values. Only +`chatMutable` properties can be set. ### Chat providers @@ -109,8 +109,8 @@ changes. Hosts that advertise [`multipleChatProviders`](/reference/common#initializeresult) accept a `provider` on `createChat`, letting one session mix agents. Forks and side -chats always use the source chat's agent. A client that does not understand -`provider` sees such chats as using the session's agent. +chats continue the source chat's history, so they always use the source chat's +agent. ## Relationship to the session channel @@ -239,8 +239,7 @@ anchor. Hosts publish `chat/movableChanged` and the corresponding ``` The result always reports the authoritative destination session. Every moved -chat keeps its URI, state (except `config`; see [Validation](#validation)), and -byte-for-byte `ChatOrigin`, so existing +chat keeps its URI, state, and byte-for-byte `ChatOrigin`, so existing chat-channel subscriptions remain valid. #### Validation @@ -256,10 +255,10 @@ durable session catalogs. The protocol does not require provider equality, prohibit active turns universally, or otherwise expose host-specific storage and runtime constraints. -A moved chat keeps its `provider`. Hosts MAY refuse, as an unsupported -destination, a move that changes whether the chat's agent matches its -session's. When a move changes a chat's `config`, clients SHOULD resubscribe to -the chat, since no action carries a changed `config.schema`. +A moved chat keeps its agent. After the move the host reports `provider` on the +chat whenever it differs from the destination session's provider, including +when the chat had none because it matched its old session. Hosts MAY refuse, as +an unsupported destination, a move they cannot support. #### Atomic synchronization diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 23f3bde66..daf596db6 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5492,12 +5492,8 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", "properties": { - "schema": { - "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." - }, "values": { "type": "object", "additionalProperties": {}, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index ea39047fb..ea920c756 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -188,7 +188,7 @@ }, "multipleChatProviders": { "$ref": "#/$defs/MultipleChatProvidersCapability", - "description": "Present when chats in one session may be backed by different agents\n(see {@link CreateChatParams.provider} and {@link ChatState.provider}).\nClients MUST NOT send `CreateChatParams.provider` without it. Hosts do not\nadvertise it below protocol `1.1.0`.\n\nChats are shared state, so a client that does not understand `provider`\nsees such a chat as using the session's agent and may offer that agent's\nsettings or models for it; hosts reject requests that are invalid for the\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Present when chats in one session may be backed by different agents\n(see {@link CreateChatParams.provider} and {@link ChatState.provider}).\nClients MUST NOT send `CreateChatParams.provider` without it.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -1438,7 +1438,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID for the new chat. Absent means the source chat's agent\nwhen `source` is present, otherwise the session's provider.\n\nRequires {@link InitializeResult.multipleChatProviders}. Hosts reject with\n`InvalidParams` a provider that is not in the root state's `agents`, whose\nagent does not advertise `multipleChats`, or that differs from the source\nchat's agent. Agent capability checks for this request use the chat's\nagent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID for the new chat. Absent means the session's provider.\n\nRequires {@link InitializeResult.multipleChatProviders}. Hosts reject with\n`InvalidParams` a provider that is not in the root state's `agents` or\nwhose agent does not advertise `multipleChats`. Forks and side chats\n(`source` present) continue the source chat's history, so they always use\nthe source chat's agent: absent means that agent, and a different value is\nrejected. Agent capability checks for this request use the chat's agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -1483,7 +1483,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY\nrefuse, as an unsupported destination, a move they cannot support, such as\none that changes whether the chat's agent matches its session's. No action\ncarries a changed `ChatConfigState.schema`, so when a move changes a chat's\n`config`, clients SHOULD resubscribe to the chat to get it.\n\nOn success every moved chat keeps its URI, state (except `config`, as\nabove), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -4777,12 +4777,8 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", "properties": { - "schema": { - "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." - }, "values": { "type": "object", "additionalProperties": {}, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index fb9ba1cc9..73d1c12cc 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2902,12 +2902,8 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", "properties": { - "schema": { - "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." - }, "values": { "type": "object", "additionalProperties": {}, @@ -6290,7 +6286,7 @@ }, "multipleChatProviders": { "$ref": "#/$defs/MultipleChatProvidersCapability", - "description": "Present when chats in one session may be backed by different agents\n(see {@link CreateChatParams.provider} and {@link ChatState.provider}).\nClients MUST NOT send `CreateChatParams.provider` without it. Hosts do not\nadvertise it below protocol `1.1.0`.\n\nChats are shared state, so a client that does not understand `provider`\nsees such a chat as using the session's agent and may offer that agent's\nsettings or models for it; hosts reject requests that are invalid for the\nchat's agent.\n\nStability: 1.0 - Early development." + "description": "Present when chats in one session may be backed by different agents\n(see {@link CreateChatParams.provider} and {@link ChatState.provider}).\nClients MUST NOT send `CreateChatParams.provider` without it.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -7540,7 +7536,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID for the new chat. Absent means the source chat's agent\nwhen `source` is present, otherwise the session's provider.\n\nRequires {@link InitializeResult.multipleChatProviders}. Hosts reject with\n`InvalidParams` a provider that is not in the root state's `agents`, whose\nagent does not advertise `multipleChats`, or that differs from the source\nchat's agent. Agent capability checks for this request use the chat's\nagent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID for the new chat. Absent means the session's provider.\n\nRequires {@link InitializeResult.multipleChatProviders}. Hosts reject with\n`InvalidParams` a provider that is not in the root state's `agents` or\nwhose agent does not advertise `multipleChats`. Forks and side chats\n(`source` present) continue the source chat's history, so they always use\nthe source chat's agent: absent means that agent, and a different value is\nrejected. Agent capability checks for this request use the chat's agent.\n\nStability: 1.0 - Early development." } }, "required": [ @@ -7585,7 +7581,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY\nrefuse, as an unsupported destination, a move they cannot support, such as\none that changes whether the chat's agent matches its session's. No action\ncarries a changed `ChatConfigState.schema`, so when a move changes a chat's\n`config`, clients SHOULD resubscribe to the chat to get it.\n\nOn success every moved chat keeps its URI, state (except `config`, as\nabove), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index b28dd0026..4e59b80e4 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3204,12 +3204,8 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", "properties": { - "schema": { - "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." - }, "values": { "type": "object", "additionalProperties": {}, diff --git a/schema/state.schema.json b/schema/state.schema.json index af66347d2..413fb2c77 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2813,12 +2813,8 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nWhen {@link schema} is present, it replaces the session's schema for this\nchat, and its property `default`s replace the session's values as the base\nthat {@link values} overlays. Hosts reject `chat/configChanged` keys that are\nnot `chatMutable` in the effective schema.", + "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", "properties": { - "schema": { - "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Hosts set it when the chat's agent differs\nfrom the session's; when present, clients use it instead of the session's\nschema. No action changes it.\n\nStability: 1.0 - Early development." - }, "values": { "type": "object", "additionalProperties": {}, diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index eaea50bf1..5403b8f24 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -121,14 +121,14 @@ export interface CreateChatParams extends BaseParams { /** Client-selected initial display title for the new chat. */ title?: string; /** - * Agent provider ID for the new chat. Absent means the source chat's agent - * when `source` is present, otherwise the session's provider. + * Agent provider ID for the new chat. Absent means the session's provider. * * Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with - * `InvalidParams` a provider that is not in the root state's `agents`, whose - * agent does not advertise `multipleChats`, or that differs from the source - * chat's agent. Agent capability checks for this request use the chat's - * agent. + * `InvalidParams` a provider that is not in the root state's `agents` or + * whose agent does not advertise `multipleChats`. Forks and side chats + * (`source` present) continue the source chat's history, so they always use + * the source chat's agent: absent means that agent, and a different value is + * rejected. Agent capability checks for this request use the chat's agent. * @stability 1.0 */ provider?: string; @@ -201,14 +201,15 @@ export type ChatMoveDestination = * Rejection leaves ownership, catalog order, chat state, and root summaries * unchanged. * - * A moved chat keeps its {@link ChatState.provider | `provider`}. Hosts MAY - * refuse, as an unsupported destination, a move they cannot support, such as - * one that changes whether the chat's agent matches its session's. No action - * carries a changed `ChatConfigState.schema`, so when a move changes a chat's - * `config`, clients SHOULD resubscribe to the chat to get it. + * A moved chat keeps its agent. After the move the host reports + * {@link ChatState.provider | `provider`} on the chat whenever it differs from + * the destination session's provider, including when the chat had none + * because it matched its old session. Hosts MAY refuse, as an unsupported + * destination, a move they cannot support, such as one that changes whether + * the chat's agent matches its session's. * - * On success every moved chat keeps its URI, state (except `config`, as - * above), and immutable `ChatOrigin`. The host commits ownership and catalog order before publishing + * On success every moved chat keeps its URI, state, and immutable + * `ChatOrigin`. The host commits ownership and catalog order before publishing * `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and * root summary updates as applicable. Session and root snapshots are the * durable recovery path after reconnect or an uncertain response. diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 4fe2e606d..1009d92cd 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -12,7 +12,6 @@ import type { AgentSelection, ChangesSummary, McpAuthRequirement, - SessionConfigSchema, SessionStatus, } from '../channels-session/state.js'; import type { Changeset } from '../channels-changeset/state.js'; @@ -440,21 +439,15 @@ export const enum ChatInteractivity { * these values applied over them. Only properties with `chatMutable: true` * in the session's config schema can be set on a chat. * - * When {@link schema} is present, it replaces the session's schema for this - * chat, and its property `default`s replace the session's values as the base - * that {@link values} overlays. Hosts reject `chat/configChanged` keys that are - * not `chatMutable` in the effective schema. + * For a chat whose {@link ChatState.provider | `provider`} differs from the + * session's, the session's config does not apply: its properties come from + * that agent's schema, which clients obtain with `resolveSessionConfig` for + * the chat's `provider`, and `values` overlays that agent's resolved values. + * Only `chatMutable` properties can be set. * * @category Chat Config Types */ export interface ChatConfigState { - /** - * Config schema for this chat. Hosts set it when the chat's agent differs - * from the session's; when present, clients use it instead of the session's - * schema. No action changes it. - * @stability 1.0 - */ - schema?: SessionConfigSchema; /** Config values set on this chat, keyed by property id */ values: Record; } diff --git a/types/common/commands.ts b/types/common/commands.ts index ae1a82953..36c00cd00 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -316,13 +316,7 @@ export interface InitializeResult { /** * Present when chats in one session may be backed by different agents * (see {@link CreateChatParams.provider} and {@link ChatState.provider}). - * Clients MUST NOT send `CreateChatParams.provider` without it. Hosts do not - * advertise it below protocol `1.1.0`. - * - * Chats are shared state, so a client that does not understand `provider` - * sees such a chat as using the session's agent and may offer that agent's - * settings or models for it; hosts reject requests that are invalid for the - * chat's agent. + * Clients MUST NOT send `CreateChatParams.provider` without it. * @stability 1.0 * @see {@link /specification/chat-channel#chat-providers | Chat Channel: Chat providers} */ diff --git a/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json b/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json deleted file mode 100644 index 213d933e5..000000000 --- a/types/test-cases/reducers/308-chat-configchanged-preserves-chat-schema.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "description": "chat/configChanged merges into the overlay values and preserves the chat's config schema", - "reducer": "chat", - "initial": { - "turns": [], - "resource": "ahp-chat:/c2", - "title": "Other agent", - "status": 1, - "modifiedAt": "2026-10-19T12:00:00.000Z", - "provider": "other-agent", - "config": { - "schema": { - "type": "object", - "properties": { - "autoApprove": { - "type": "boolean", - "title": "Auto-approve tools", - "default": false, - "chatMutable": true - } - } - }, - "values": { - "autoApprove": false - } - } - }, - "actions": [ - { - "type": "chat/configChanged", - "config": { - "autoApprove": true - } - } - ], - "expected": { - "turns": [], - "resource": "ahp-chat:/c2", - "title": "Other agent", - "status": 1, - "modifiedAt": "2026-10-19T12:00:00.000Z", - "provider": "other-agent", - "config": { - "schema": { - "type": "object", - "properties": { - "autoApprove": { - "type": "boolean", - "title": "Auto-approve tools", - "default": false, - "chatMutable": true - } - } - }, - "values": { - "autoApprove": true - } - } - } -} diff --git a/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json b/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json deleted file mode 100644 index 94dcea428..000000000 --- a/types/test-cases/reducers/310-chat-configchanged-replace-preserves-chat-schema.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "description": "chat/configChanged with replace replaces the overlay values and preserves the chat's config schema", - "reducer": "chat", - "initial": { - "turns": [], - "resource": "ahp-chat:/c2", - "title": "Other agent", - "status": 1, - "modifiedAt": "2026-10-19T12:00:00.000Z", - "provider": "other-agent", - "config": { - "schema": { - "type": "object", - "properties": { - "autoApprove": { - "type": "boolean", - "title": "Auto-approve tools", - "default": false, - "chatMutable": true - } - } - }, - "values": { - "autoApprove": true - } - } - }, - "actions": [ - { - "type": "chat/configChanged", - "config": {}, - "replace": true - } - ], - "expected": { - "turns": [], - "resource": "ahp-chat:/c2", - "title": "Other agent", - "status": 1, - "modifiedAt": "2026-10-19T12:00:00.000Z", - "provider": "other-agent", - "config": { - "schema": { - "type": "object", - "properties": { - "autoApprove": { - "type": "boolean", - "title": "Auto-approve tools", - "default": false, - "chatMutable": true - } - } - }, - "values": {} - } - } -} diff --git a/types/test-cases/round-trips/056-chat-config-snapshot.json b/types/test-cases/round-trips/056-chat-config-snapshot.json index 725520488..c22144f61 100644 --- a/types/test-cases/round-trips/056-chat-config-snapshot.json +++ b/types/test-cases/round-trips/056-chat-config-snapshot.json @@ -1,7 +1,7 @@ { "name": "chat-config-snapshot", "group": "A", - "description": "ChatState.config is an overlay that carries only the chat's own config values when it has no schema; it survives a decode/re-encode cycle on a chat snapshot.", + "description": "ChatState.config is an overlay that carries only the chat's own config values (no schema); it survives a decode/re-encode cycle on a chat snapshot.", "type": "Snapshot", "input": { "resource": "ahp-chat:/peer", diff --git a/types/test-cases/round-trips/079-chat-config-schema-snapshot.json b/types/test-cases/round-trips/079-chat-config-schema-snapshot.json deleted file mode 100644 index c0cb4118e..000000000 --- a/types/test-cases/round-trips/079-chat-config-schema-snapshot.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "chat-config-schema-snapshot", - "group": "A", - "description": "A chat snapshot preserves a per-chat config schema for a chat backed by a different agent than its session.", - "type": "Snapshot", - "input": { - "resource": "ahp-chat:/c2", - "state": { - "turns": [], - "resource": "ahp-chat:/c2", - "title": "Other agent", - "status": 1, - "modifiedAt": "2026-10-19T12:00:00.000Z", - "provider": "other-agent", - "config": { - "schema": { - "type": "object", - "properties": { - "autoApprove": { - "type": "boolean", - "title": "Auto-approve tools", - "default": false, - "chatMutable": true - } - } - }, - "values": { - "autoApprove": false - } - } - }, - "fromSeq": 4 - }, - "acceptableOutputs": [ - { - "resource": "ahp-chat:/c2", - "state": { - "turns": [], - "resource": "ahp-chat:/c2", - "title": "Other agent", - "status": 1, - "modifiedAt": "2026-10-19T12:00:00.000Z", - "provider": "other-agent", - "config": { - "schema": { - "type": "object", - "properties": { - "autoApprove": { - "type": "boolean", - "title": "Auto-approve tools", - "default": false, - "chatMutable": true - } - } - }, - "values": { - "autoApprove": false - } - } - }, - "fromSeq": 4 - } - ] -} From 6fc80fff21885796d9961d4a7f7b1d3fcff6f0bf Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 23 Oct 2026 18:07:50 +0200 Subject: [PATCH 08/13] chat: per-chat config is the chat's complete config Chat config no longer inherits from the session: - `ChatConfigState.values` is always the chat's complete config; a session config change never changes a chat's. Missing keys take the effective schema's `default`. - Optional `ChatConfigState.schema`: absent means the session's schema; hosts set it when the chat's provider differs from the session's. - `chatMutable` still gates `chat/configChanged`; hosts set initial values. moveChat keeps values; hosts set/clear schema or refuse. - Removes the `resolveSessionConfig` sentence; updates the unreleased chat-config fragment, docs, and fixture descriptions; restores the schema fixtures and the Kotlin/Rust reducers that keep `schema`. - Renumbers this PR's fixtures after those added on main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Actions.generated.cs | 8 +-- .../Generated/Commands.generated.cs | 5 ++ .../Generated/State.generated.cs | 37 +++++------ clients/go/ahptypes/actions.generated.go | 8 +-- clients/go/ahptypes/commands.generated.go | 5 ++ clients/go/ahptypes/state.generated.go | 35 +++++----- .../microsoft/agenthostprotocol/Reducers.kt | 2 +- .../generated/Commands.generated.kt | 4 +- .../generated/State.generated.kt | 16 +++-- clients/rust/crates/ahp-types/src/actions.rs | 8 +-- clients/rust/crates/ahp-types/src/commands.rs | 5 ++ clients/rust/crates/ahp-types/src/state.rs | 36 +++++------ clients/rust/crates/ahp/src/reducers.rs | 1 + .../Generated/Commands.generated.swift | 4 +- .../Generated/State.generated.swift | 16 +++-- docs/.changes/20261014-chat-config.json | 2 +- docs/guide/actions.md | 2 +- docs/guide/state-model.md | 11 ++-- docs/specification/chat-channel.md | 38 +++++------ schema/actions.schema.json | 14 ++-- schema/commands.schema.json | 16 +++-- schema/errors.schema.json | 16 +++-- schema/notifications.schema.json | 12 ++-- schema/state.schema.json | 12 ++-- types/channels-chat/actions.ts | 8 +-- types/channels-chat/commands.ts | 5 ++ types/channels-chat/state.ts | 35 +++++----- types/channels-session/state.ts | 4 +- ...nfigchanged-merges-into-config-values.json | 2 +- ...igchanged-replace-replaces-all-values.json | 2 +- ...hanged-creates-config-when-undefined.json} | 2 +- ...without-key-resets-to-schema-default.json} | 2 +- ...session-chatupdated-ignores-provider.json} | 0 ...d-replace-creates-config-when-absent.json} | 2 +- ...t-configchanged-preserves-chat-schema.json | 60 +++++++++++++++++ ...changed-replace-preserves-chat-schema.json | 57 +++++++++++++++++ .../round-trips/056-chat-config-snapshot.json | 2 +- ...der.json => 079-create-chat-provider.json} | 0 ...0-initialize-multiple-chat-providers.json} | 0 ...er.json => 081-chat-summary-provider.json} | 0 ...ion-chat-summary-provider-round-trip.json} | 0 .../083-chat-config-schema-snapshot.json | 64 +++++++++++++++++++ 42 files changed, 399 insertions(+), 159 deletions(-) rename types/test-cases/reducers/{291-chat-configchanged-creates-overlay-when-config-undefined.json => 291-chat-configchanged-creates-config-when-undefined.json} (85%) rename types/test-cases/reducers/{292-chat-configchanged-replace-without-key-resets-to-session-value.json => 292-chat-configchanged-replace-without-key-resets-to-schema-default.json} (83%) rename types/test-cases/reducers/{309-session-chatupdated-ignores-provider.json => 316-session-chatupdated-ignores-provider.json} (100%) rename types/test-cases/reducers/{311-chat-configchanged-replace-creates-config-when-absent.json => 317-chat-configchanged-replace-creates-config-when-absent.json} (86%) create mode 100644 types/test-cases/reducers/318-chat-configchanged-preserves-chat-schema.json create mode 100644 types/test-cases/reducers/319-chat-configchanged-replace-preserves-chat-schema.json rename types/test-cases/round-trips/{076-create-chat-provider.json => 079-create-chat-provider.json} (100%) rename types/test-cases/round-trips/{077-initialize-multiple-chat-providers.json => 080-initialize-multiple-chat-providers.json} (100%) rename types/test-cases/round-trips/{078-chat-summary-provider.json => 081-chat-summary-provider.json} (100%) rename types/test-cases/round-trips/{080-session-chat-summary-provider-round-trip.json => 082-session-chat-summary-provider-round-trip.json} (100%) create mode 100644 types/test-cases/round-trips/083-chat-config-schema-snapshot.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index 34bb7d9d5..f687d1e51 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -2092,10 +2092,10 @@ public sealed record ChatIsArchivedChangedAction /// Config values set on a chat changed. /// /// Merges `config` into the chat's -/// {@link ChatConfigState | `ChatState.config`} overlay, or replaces the -/// overlay when `replace` is `true`. Only properties with `chatMutable: true` -/// in the session's config schema may be set. To reset a property to the -/// session's value, replace the overlay with one that omits the property. +/// {@link ChatConfigState | `ChatState.config`} values, or sets the complete +/// values when `replace` is `true`; keys missing after a replace take the +/// effective schema's `default`. Only properties with `chatMutable: true` in +/// the effective schema may be set. public sealed record ChatConfigChangedAction { public ActionType Type { get; init; } = ActionType.ChatConfigChanged; diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 0da7d0fcc..b84b92cf1 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -1114,6 +1114,11 @@ public sealed record ChatMoveToNewSessionDestination /// destination, a move they cannot support, such as one that changes whether /// the chat's agent matches its session's. /// +/// A moved chat's `config.values` move with it unchanged. If a move changes +/// whether the chat needs its own `config.schema`, the host either sets or +/// clears it, and clients SHOULD resubscribe to the chat since no action +/// carries a schema change, or refuses the move as an unsupported destination. +/// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 2e7b7692d..c9df09b63 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2552,9 +2552,7 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Canvases { get; set; } - /// Configuration values set on this chat, applied over the owning session's - /// {@link SessionState.config | `SessionState.config`}. See - /// {@link ChatConfigState}. + /// This chat's configuration. See {@link ChatConfigState}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatConfigState? Config { get; set; } @@ -3486,8 +3484,8 @@ public sealed record SessionConfigPropertySchema [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? SessionMutable { get; init; } - /// When `true`, the property may be set per chat with `chat/configChanged`. - /// The chat's value overrides the session's value for that chat only. + /// When `true`, the property may be changed per chat with + /// `chat/configChanged`, affecting only that chat. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? ChatMutable { get; init; } } @@ -3519,22 +3517,25 @@ public sealed class SessionConfigState public required Dictionary Values { get; set; } } -/// Configuration values set on a chat: an overlay on the owning session's -/// {@link SessionState.config | `SessionState.config`}. +/// A chat's configuration. It does not inherit from the session: changing the +/// session's config never changes a chat's. /// -/// It carries only the chat's own values; the session's config schema -/// applies. The effective config of a chat is the session's values with -/// these values applied over them. Only properties with `chatMutable: true` -/// in the session's config schema can be set on a chat. -/// -/// For a chat whose {@link ChatState.provider | `provider`} differs from the -/// session's, the session's config does not apply: its properties come from -/// that agent's schema, which clients obtain with `resolveSessionConfig` for -/// the chat's `provider`, and `values` overlays that agent's resolved values. -/// Only `chatMutable` properties can be set. +/// Only properties with `chatMutable: true` in the effective schema can be +/// changed with `chat/configChanged`; hosts reject other keys. Hosts set the +/// initial `values` when they create the chat. public sealed class ChatConfigState { - /// Config values set on this chat, keyed by property id + /// Config schema for this chat. Absent means the session's schema. Hosts set + /// it when the chat's {@link ChatState.provider | `provider`} differs from + /// the session's; when present, clients use it instead of the session's + /// schema. + /// + /// Stability: 1.0 - Early development. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionConfigSchema? Schema { get; set; } + + /// The chat's complete config, keyed by property id. A missing key takes the + /// effective schema's `default`. public required Dictionary Values { get; set; } } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index e56555da7..44e7b0073 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -877,10 +877,10 @@ type ChatIsArchivedChangedAction struct { // Config values set on a chat changed. // // Merges `config` into the chat's -// {@link ChatConfigState | `ChatState.config`} overlay, or replaces the -// overlay when `replace` is `true`. Only properties with `chatMutable: true` -// in the session's config schema may be set. To reset a property to the -// session's value, replace the overlay with one that omits the property. +// {@link ChatConfigState | `ChatState.config`} values, or sets the complete +// values when `replace` is `true`; keys missing after a replace take the +// effective schema's `default`. Only properties with `chatMutable: true` in +// the effective schema may be set. type ChatConfigChangedAction struct { Type ActionType `json:"type"` // Config values to set on the chat diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 38e1ca495..57dd0e3ed 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -737,6 +737,11 @@ type ChatMoveToNewSessionDestination struct { // destination, a move they cannot support, such as one that changes whether // the chat's agent matches its session's. // +// A moved chat's `config.values` move with it unchanged. If a move changes +// whether the chat needs its own `config.schema`, the host either sets or +// clears it, and clients SHOULD resubscribe to the chat since no action +// carries a schema change, or refuses the move as an unsupported destination. +// // On success every moved chat keeps its URI, state, and immutable // `ChatOrigin`. The host commits ownership and catalog order before publishing // `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 279a9f826..409aceb34 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1440,9 +1440,7 @@ type ChatState struct { // Clients subscribe to each resource for the experimental presentation // state, including its current live source URL. Canvases []CanvasReference `json:"canvases,omitempty"` - // Configuration values set on this chat, applied over the owning session's - // {@link SessionState.config | `SessionState.config`}. See - // {@link ChatConfigState}. + // This chat's configuration. See {@link ChatConfigState}. Config *ChatConfigState `json:"config,omitempty"` // Completed turns Turns []Turn `json:"turns"` @@ -1682,8 +1680,8 @@ type SessionConfigPropertySchema struct { EnumDynamic *bool `json:"enumDynamic,omitempty"` // When `true`, the user may change this property after session creation SessionMutable *bool `json:"sessionMutable,omitempty"` - // When `true`, the property may be set per chat with `chat/configChanged`. - // The chat's value overrides the session's value for that chat only. + // When `true`, the property may be changed per chat with + // `chat/configChanged`, affecting only that chat. ChatMutable *bool `json:"chatMutable,omitempty"` } @@ -1708,21 +1706,22 @@ type SessionConfigState struct { Values map[string]json.RawMessage `json:"values"` } -// Configuration values set on a chat: an overlay on the owning session's -// {@link SessionState.config | `SessionState.config`}. +// A chat's configuration. It does not inherit from the session: changing the +// session's config never changes a chat's. // -// It carries only the chat's own values; the session's config schema -// applies. The effective config of a chat is the session's values with -// these values applied over them. Only properties with `chatMutable: true` -// in the session's config schema can be set on a chat. -// -// For a chat whose {@link ChatState.provider | `provider`} differs from the -// session's, the session's config does not apply: its properties come from -// that agent's schema, which clients obtain with `resolveSessionConfig` for -// the chat's `provider`, and `values` overlays that agent's resolved values. -// Only `chatMutable` properties can be set. +// Only properties with `chatMutable: true` in the effective schema can be +// changed with `chat/configChanged`; hosts reject other keys. Hosts set the +// initial `values` when they create the chat. type ChatConfigState struct { - // Config values set on this chat, keyed by property id + // Config schema for this chat. Absent means the session's schema. Hosts set + // it when the chat's {@link ChatState.provider | `provider`} differs from + // the session's; when present, clients use it instead of the session's + // schema. + // + // Stability: 1.0 - Early development. + Schema *SessionConfigSchema `json:"schema,omitempty"` + // The chat's complete config, keyed by property id. A missing key takes the + // effective schema's `default`. Values map[string]json.RawMessage `json:"values"` } 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 6aaa44802..04f6335b1 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -1634,7 +1634,7 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when val a = action.value val current = state.config val newValues = if (a.replace == true || current == null) a.config else current.values + a.config - state.copy(config = ChatConfigState(values = newValues)) + state.copy(config = current?.copy(values = newValues) ?: ChatConfigState(values = newValues)) } else -> state 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 d57eb574f..c31e27366 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 @@ -1734,8 +1734,8 @@ data class SessionConfigPropertySchema( */ val sessionMutable: Boolean? = null, /** - * When `true`, the property may be set per chat with `chat/configChanged`. - * The chat's value overrides the session's value for that chat only. + * When `true`, the property may be changed per chat with + * `chat/configChanged`, affecting only that chat. */ val chatMutable: Boolean? = 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 f1589f7b9..78f0b4f9d 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 @@ -1866,9 +1866,7 @@ data class ChatState( */ val canvases: List? = null, /** - * Configuration values set on this chat, applied over the owning session's - * {@link SessionState.config | `SessionState.config`}. See - * {@link ChatConfigState}. + * This chat's configuration. See {@link ChatConfigState}. */ val config: ChatConfigState? = null, /** @@ -2570,7 +2568,17 @@ data class SessionConfigState( @Serializable data class ChatConfigState( /** - * Config values set on this chat, keyed by property id + * Config schema for this chat. Absent means the session's schema. Hosts set + * it when the chat's {@link ChatState.provider | `provider`} differs from + * the session's; when present, clients use it instead of the session's + * schema. + * + * Stability: 1.0 - Early development. + */ + val schema: SessionConfigSchema? = null, + /** + * The chat's complete config, keyed by property id. A missing key takes the + * effective schema's `default`. */ val values: Map ) diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index d3063a983..ad3f7abb9 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1554,10 +1554,10 @@ pub struct ChatIsArchivedChangedAction { /// Config values set on a chat changed. /// /// Merges `config` into the chat's -/// {@link ChatConfigState | `ChatState.config`} overlay, or replaces the -/// overlay when `replace` is `true`. Only properties with `chatMutable: true` -/// in the session's config schema may be set. To reset a property to the -/// session's value, replace the overlay with one that omits the property. +/// {@link ChatConfigState | `ChatState.config`} values, or sets the complete +/// values when `replace` is `true`; keys missing after a replace take the +/// effective schema's `default`. Only properties with `chatMutable: true` in +/// the effective schema may be set. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigChangedAction { diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 4c536f9c4..adcb779e6 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -1023,6 +1023,11 @@ pub struct ChatMoveToNewSessionDestination {} /// destination, a move they cannot support, such as one that changes whether /// the chat's agent matches its session's. /// +/// A moved chat's `config.values` move with it unchanged. If a move changes +/// whether the chat needs its own `config.schema`, the host either sets or +/// clears it, and clients SHOULD resubscribe to the chat since no action +/// carries a schema change, or refuses the move as an unsupported destination. +/// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing /// `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index aef37d928..498a34765 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2112,9 +2112,7 @@ pub struct ChatState { /// state, including its current live source URL. #[serde(default, skip_serializing_if = "Option::is_none")] pub canvases: Option>, - /// Configuration values set on this chat, applied over the owning session's - /// {@link SessionState.config | `SessionState.config`}. See - /// {@link ChatConfigState}. + /// This chat's configuration. See {@link ChatConfigState}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Completed turns @@ -2859,8 +2857,8 @@ pub struct SessionConfigPropertySchema { /// When `true`, the user may change this property after session creation #[serde(default, skip_serializing_if = "Option::is_none")] pub session_mutable: Option, - /// When `true`, the property may be set per chat with `chat/configChanged`. - /// The chat's value overrides the session's value for that chat only. + /// When `true`, the property may be changed per chat with + /// `chat/configChanged`, affecting only that chat. #[serde(default, skip_serializing_if = "Option::is_none")] pub chat_mutable: Option, } @@ -2891,23 +2889,25 @@ pub struct SessionConfigState { pub values: JsonObject, } -/// Configuration values set on a chat: an overlay on the owning session's -/// {@link SessionState.config | `SessionState.config`}. +/// A chat's configuration. It does not inherit from the session: changing the +/// session's config never changes a chat's. /// -/// It carries only the chat's own values; the session's config schema -/// applies. The effective config of a chat is the session's values with -/// these values applied over them. Only properties with `chatMutable: true` -/// in the session's config schema can be set on a chat. -/// -/// For a chat whose {@link ChatState.provider | `provider`} differs from the -/// session's, the session's config does not apply: its properties come from -/// that agent's schema, which clients obtain with `resolveSessionConfig` for -/// the chat's `provider`, and `values` overlays that agent's resolved values. -/// Only `chatMutable` properties can be set. +/// Only properties with `chatMutable: true` in the effective schema can be +/// changed with `chat/configChanged`; hosts reject other keys. Hosts set the +/// initial `values` when they create the chat. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { - /// Config values set on this chat, keyed by property id + /// Config schema for this chat. Absent means the session's schema. Hosts set + /// it when the chat's {@link ChatState.provider | `provider`} differs from + /// the session's; when present, clients use it instead of the session's + /// schema. + /// + /// Stability: 1.0 - Early development. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schema: Option, + /// The chat's complete config, keyed by property id. A missing key takes the + /// effective schema's `default`. pub values: JsonObject, } diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index c4a63b1f3..79cae8a8f 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -1421,6 +1421,7 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu } StateAction::ChatConfigChanged(a) => { let config = state.config.get_or_insert_with(|| ChatConfigState { + schema: None, values: Default::default(), }); if a.replace.unwrap_or(false) { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 9bfb9544a..b30ec44df 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -2059,8 +2059,8 @@ public struct SessionConfigPropertySchema: Codable, Sendable { public var enumDynamic: Bool? /// When `true`, the user may change this property after session creation public var sessionMutable: Bool? - /// When `true`, the property may be set per chat with `chat/configChanged`. - /// The chat's value overrides the session's value for that chat only. + /// When `true`, the property may be changed per chat with + /// `chat/configChanged`, affecting only that chat. public var chatMutable: Bool? enum CodingKeys: String, CodingKey { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 9a86df1db..2c19a5b62 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1866,9 +1866,7 @@ public struct ChatState: Codable, Sendable { /// Clients subscribe to each resource for the experimental presentation /// state, including its current live source URL. public var canvases: [CanvasReference]? - /// Configuration values set on this chat, applied over the owning session's - /// {@link SessionState.config | `SessionState.config`}. See - /// {@link ChatConfigState}. + /// This chat's configuration. See {@link ChatConfigState}. public var config: ChatConfigState? /// Completed turns public var turns: [Turn] @@ -2693,12 +2691,22 @@ public struct SessionConfigState: Codable, Sendable { } public struct ChatConfigState: Codable, Sendable { - /// Config values set on this chat, keyed by property id + /// Config schema for this chat. Absent means the session's schema. Hosts set + /// it when the chat's {@link ChatState.provider | `provider`} differs from + /// the session's; when present, clients use it instead of the session's + /// schema. + /// + /// Stability: 1.0 - Early development. + public var schema: SessionConfigSchema? + /// The chat's complete config, keyed by property id. A missing key takes the + /// effective schema's `default`. public var values: [String: AnyCodable] public init( + schema: SessionConfigSchema? = nil, values: [String: AnyCodable] ) { + self.schema = schema self.values = values } } diff --git a/docs/.changes/20261014-chat-config.json b/docs/.changes/20261014-chat-config.json index 77e9350c9..a18c507ca 100644 --- a/docs/.changes/20261014-chat-config.json +++ b/docs/.changes/20261014-chat-config.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "`ChatState.config` overlay, the `chat/configChanged` action, and `chatMutable` on session config properties let a host allow per-chat values for selected settings, such as tool auto-approval, so changing them in one chat no longer changes every chat in the session." + "message": "`ChatState.config`, the `chat/configChanged` action, and `chatMutable` on session config properties give each chat its own config for selected settings, such as tool auto-approval, so changing them in one chat no longer changes every chat in the session. `ChatConfigState.values` is the chat's complete config and does not inherit from the session; missing keys take the schema default. Optional `ChatConfigState.schema` replaces the session's schema for a chat whose agent differs from the session's." } diff --git a/docs/guide/actions.md b/docs/guide/actions.md index 408af695b..9e4a5ef24 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -218,7 +218,7 @@ The client applies the action **optimistically** to its local state before sendi | `session/isReadChanged` | Marks the session as read or unread | | `chat/isReadChanged` | Marks any known chat, including the default chat, as read or unread without changing its owning session or sibling chats; refreshes the compact `SessionChatSummary.status` projection | | `session/isArchivedChanged` | Archives or unarchives the session | -| `chat/configChanged` | Sets `chatMutable` config values for this chat, overriding the session's values for this chat only | +| `chat/configChanged` | Sets `chatMutable` config values for this chat only | ## Reducers diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 7a546a4e0..82eb45856 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -195,7 +195,7 @@ ChatState { provider?: string // chat's agent; absent = session's provider workingDirectories?: URI[] // subset of session's workingDirectories changesets?: Changeset[] // per-chat Branch, Uncommitted Changes, etc. - config?: ChatConfigState // config values set on this chat (overlay) + config?: ChatConfigState // chat's complete config (+ optional schema) turns: Turn[] // completed turns turnsNextCursor?: string // page older turns via fetchTurns @@ -213,11 +213,10 @@ changeset channel contract. Hosts scope a chat's catalogue to its effective working directories: the chat's subset when present, otherwise the full session set. -`config` is an overlay on `SessionState.config`: it carries only the values set -on this chat, and the session's config schema applies. A chat's effective -config is the session's values with the chat's values applied over them. Only -properties marked `chatMutable: true` in the session's schema can be set per -chat, with `chat/configChanged`. See +`config.values` is the chat's complete config; it does not inherit from the +session. `config.schema` is present only when the chat's agent differs from the +session's; otherwise the session's schema applies. Only `chatMutable` +properties can be changed, with `chat/configChanged`. See [Per-chat configuration](/specification/chat-channel#per-chat-configuration). `provider` names the chat's agent when it differs from the session's. Hosts diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index b49c671d4..97faebca4 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -78,26 +78,21 @@ fully replaces the catalogue or clears it when `changesets` is absent. ### Per-chat configuration -[`ChatState.config`](/reference/chat#chatstate) is an overlay on the owning -session's [`SessionState.config`](/reference/session#sessionstate). It carries -only the values set on this chat; the session's config schema applies. A -chat's effective config is the session's values with the chat's values -applied over them. Hosts do not mirror session config into chats. - -A host opts a property in to per-chat values by setting -[`chatMutable: true`](/reference/session#sessionconfigpropertyschema) on it in -the session's config schema. Clients set such properties for one chat by +[`ChatState.config`](/reference/chat#chatstate) is the chat's own +configuration. `values` is always its complete config, set by the host when it +creates the chat; it does not inherit from the session, so changing the +session's config never changes a chat's. A key missing from `values` takes the +effective schema's `default`. + +The effective schema is `config.schema` when present, otherwise the session's +schema. Hosts set `config.schema` only for a chat whose `provider` differs from +the session's. + +Clients change a chat's +[`chatMutable`](/reference/session#sessionconfigpropertyschema) properties by dispatching [`chat/configChanged`](/reference/chat#chatconfigchangedaction), -which merges into the chat's overlay or replaces it when `replace` is `true`. -To reset a property to the session's value, replace the overlay with one that -omits the property. Every other property is changed on the session with -`session/configChanged`. - -For a chat whose `provider` differs from the session's, the session's config -does not apply: its properties come from that agent's schema, which clients -obtain with [`resolveSessionConfig`](/reference/root#resolvesessionconfig) for -the chat's `provider`, and `values` overlays that agent's resolved values. Only -`chatMutable` properties can be set. +which merges into `values` or replaces them when `replace` is `true`. Hosts +reject keys that are not `chatMutable` in the effective schema. ### Chat providers @@ -260,6 +255,11 @@ chat whenever it differs from the destination session's provider, including when the chat had none because it matched its old session. Hosts MAY refuse, as an unsupported destination, a move they cannot support. +A moved chat's `config.values` move with it unchanged. If a move changes whether +the chat needs its own `config.schema`, the host either sets or clears it +(clients SHOULD resubscribe, since no action carries a schema change) or +refuses the move as an unsupported destination. + #### Atomic synchronization The host validates and persists ownership, its internal hierarchy, catalog diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 7f68610c1..ad52a534f 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -1714,7 +1714,7 @@ }, "ChatConfigChangedAction": { "type": "object", - "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} overlay, or replaces the\noverlay when `replace` is `true`. Only properties with `chatMutable: true`\nin the session's config schema may be set. To reset a property to the\nsession's value, replace the overlay with one that omits the property.", + "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or sets the complete\nvalues when `replace` is `true`; keys missing after a replace take the\neffective schema's `default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", "properties": { "type": { "const": "chat/configChanged" @@ -4053,7 +4053,7 @@ }, "chatMutable": { "type": "boolean", - "description": "When `true`, the property may be set per chat with `chat/configChanged`.\nThe chat's value overrides the session's value for that chat only." + "description": "When `true`, the property may be changed per chat with\n`chat/configChanged`, affecting only that chat." } }, "required": [ @@ -5262,7 +5262,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "This chat's configuration. See {@link ChatConfigState}." }, "turns": { "type": "array", @@ -5492,12 +5492,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", + "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 587355170..2eb1dddcf 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1509,7 +1509,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host either sets or\nclears it, and clients SHOULD resubscribe to the chat since no action\ncarries a schema change, or refuses the move as an unsupported destination.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -3374,7 +3374,7 @@ }, "chatMutable": { "type": "boolean", - "description": "When `true`, the property may be set per chat with `chat/configChanged`.\nThe chat's value overrides the session's value for that chat only." + "description": "When `true`, the property may be changed per chat with\n`chat/configChanged`, affecting only that chat." } }, "required": [ @@ -4583,7 +4583,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "This chat's configuration. See {@link ChatConfigState}." }, "turns": { "type": "array", @@ -4813,12 +4813,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", + "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." } }, "required": [ @@ -9731,7 +9735,7 @@ }, "ChatConfigChangedAction": { "type": "object", - "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} overlay, or replaces the\noverlay when `replace` is `true`. Only properties with `chatMutable: true`\nin the session's config schema may be set. To reset a property to the\nsession's value, replace the overlay with one that omits the property.", + "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or sets the complete\nvalues when `replace` is `true`; keys missing after a replace take the\neffective schema's `default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", "properties": { "type": { "const": "chat/configChanged" diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 208705a8b..a54c1b49b 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -1463,7 +1463,7 @@ }, "chatMutable": { "type": "boolean", - "description": "When `true`, the property may be set per chat with `chat/configChanged`.\nThe chat's value overrides the session's value for that chat only." + "description": "When `true`, the property may be changed per chat with\n`chat/configChanged`, affecting only that chat." } }, "required": [ @@ -2672,7 +2672,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "This chat's configuration. See {@link ChatConfigState}." }, "turns": { "type": "array", @@ -2902,12 +2902,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", + "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." } }, "required": [ @@ -7615,7 +7619,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host either sets or\nclears it, and clients SHOULD resubscribe to the chat since no action\ncarries a schema change, or refuses the move as an unsupported destination.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -10787,7 +10791,7 @@ }, "ChatConfigChangedAction": { "type": "object", - "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} overlay, or replaces the\noverlay when `replace` is `true`. Only properties with `chatMutable: true`\nin the session's config schema may be set. To reset a property to the\nsession's value, replace the overlay with one that omits the property.", + "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or sets the complete\nvalues when `replace` is `true`; keys missing after a replace take the\neffective schema's `default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", "properties": { "type": { "const": "chat/configChanged" diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 707ad6177..060bee4ed 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -1765,7 +1765,7 @@ }, "chatMutable": { "type": "boolean", - "description": "When `true`, the property may be set per chat with `chat/configChanged`.\nThe chat's value overrides the session's value for that chat only." + "description": "When `true`, the property may be changed per chat with\n`chat/configChanged`, affecting only that chat." } }, "required": [ @@ -2974,7 +2974,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "This chat's configuration. See {@link ChatConfigState}." }, "turns": { "type": "array", @@ -3204,12 +3204,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", + "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 53c644754..ca5f22314 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -1374,7 +1374,7 @@ }, "chatMutable": { "type": "boolean", - "description": "When `true`, the property may be set per chat with `chat/configChanged`.\nThe chat's value overrides the session's value for that chat only." + "description": "When `true`, the property may be changed per chat with\n`chat/configChanged`, affecting only that chat." } }, "required": [ @@ -2583,7 +2583,7 @@ }, "config": { "$ref": "#/$defs/ChatConfigState", - "description": "Configuration values set on this chat, applied over the owning session's\n{@link SessionState.config | `SessionState.config`}. See\n{@link ChatConfigState}." + "description": "This chat's configuration. See {@link ChatConfigState}." }, "turns": { "type": "array", @@ -2813,12 +2813,16 @@ }, "ChatConfigState": { "type": "object", - "description": "Configuration values set on a chat: an overlay on the owning session's\n{@link SessionState.config | `SessionState.config`}.\n\nIt carries only the chat's own values; the session's config schema\napplies. The effective config of a chat is the session's values with\nthese values applied over them. Only properties with `chatMutable: true`\nin the session's config schema can be set on a chat.\n\nFor a chat whose {@link ChatState.provider | `provider`} differs from the\nsession's, the session's config does not apply: its properties come from\nthat agent's schema, which clients obtain with `resolveSessionConfig` for\nthe chat's `provider`, and `values` overlays that agent's resolved values.\nOnly `chatMutable` properties can be set.", + "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + }, "values": { "type": "object", "additionalProperties": {}, - "description": "Config values set on this chat, keyed by property id" + "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." } }, "required": [ diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index ab109f881..75733304d 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -905,10 +905,10 @@ export interface ChatIsArchivedChangedAction { * Config values set on a chat changed. * * Merges `config` into the chat's - * {@link ChatConfigState | `ChatState.config`} overlay, or replaces the - * overlay when `replace` is `true`. Only properties with `chatMutable: true` - * in the session's config schema may be set. To reset a property to the - * session's value, replace the overlay with one that omits the property. + * {@link ChatConfigState | `ChatState.config`} values, or sets the complete + * values when `replace` is `true`; keys missing after a replace take the + * effective schema's `default`. Only properties with `chatMutable: true` in + * the effective schema may be set. * * @category Chat Actions * @version 1 diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 5403b8f24..585e7517d 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -208,6 +208,11 @@ export type ChatMoveDestination = * destination, a move they cannot support, such as one that changes whether * the chat's agent matches its session's. * + * A moved chat's `config.values` move with it unchanged. If a move changes + * whether the chat needs its own `config.schema`, the host either sets or + * clears it, and clients SHOULD resubscribe to the chat since no action + * carries a schema change, or refuses the move as an unsupported destination. + * * On success every moved chat keeps its URI, state, and immutable * `ChatOrigin`. The host commits ownership and catalog order before publishing * `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 1009d92cd..20ff5def1 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -12,6 +12,7 @@ import type { AgentSelection, ChangesSummary, McpAuthRequirement, + SessionConfigSchema, SessionStatus, } from '../channels-session/state.js'; import type { Changeset } from '../channels-changeset/state.js'; @@ -136,9 +137,7 @@ export interface ChatState { */ canvases?: CanvasReference[]; /** - * Configuration values set on this chat, applied over the owning session's - * {@link SessionState.config | `SessionState.config`}. See - * {@link ChatConfigState}. + * This chat's configuration. See {@link ChatConfigState}. */ config?: ChatConfigState; @@ -431,24 +430,28 @@ export const enum ChatInteractivity { // ─── Chat Config Types ─────────────────────────────────────────────────────── /** - * Configuration values set on a chat: an overlay on the owning session's - * {@link SessionState.config | `SessionState.config`}. + * A chat's configuration. It does not inherit from the session: changing the + * session's config never changes a chat's. * - * It carries only the chat's own values; the session's config schema - * applies. The effective config of a chat is the session's values with - * these values applied over them. Only properties with `chatMutable: true` - * in the session's config schema can be set on a chat. - * - * For a chat whose {@link ChatState.provider | `provider`} differs from the - * session's, the session's config does not apply: its properties come from - * that agent's schema, which clients obtain with `resolveSessionConfig` for - * the chat's `provider`, and `values` overlays that agent's resolved values. - * Only `chatMutable` properties can be set. + * Only properties with `chatMutable: true` in the effective schema can be + * changed with `chat/configChanged`; hosts reject other keys. Hosts set the + * initial `values` when they create the chat. * * @category Chat Config Types */ export interface ChatConfigState { - /** Config values set on this chat, keyed by property id */ + /** + * Config schema for this chat. Absent means the session's schema. Hosts set + * it when the chat's {@link ChatState.provider | `provider`} differs from + * the session's; when present, clients use it instead of the session's + * schema. + * @stability 1.0 + */ + schema?: SessionConfigSchema; + /** + * The chat's complete config, keyed by property id. A missing key takes the + * effective schema's `default`. + */ values: Record; } diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 87338508d..e9eae9935 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -627,8 +627,8 @@ export interface SessionConfigPropertySchema extends ConfigPropertySchema { /** When `true`, the user may change this property after session creation */ sessionMutable?: boolean; /** - * When `true`, the property may be set per chat with `chat/configChanged`. - * The chat's value overrides the session's value for that chat only. + * When `true`, the property may be changed per chat with + * `chat/configChanged`, affecting only that chat. */ chatMutable?: boolean; } diff --git a/types/test-cases/reducers/289-chat-configchanged-merges-into-config-values.json b/types/test-cases/reducers/289-chat-configchanged-merges-into-config-values.json index c852c71a9..c5fe02ff1 100644 --- a/types/test-cases/reducers/289-chat-configchanged-merges-into-config-values.json +++ b/types/test-cases/reducers/289-chat-configchanged-merges-into-config-values.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged merges new values into the chat's config overlay", + "description": "chat/configChanged merges new values into the chat's config values", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/290-chat-configchanged-replace-replaces-all-values.json b/types/test-cases/reducers/290-chat-configchanged-replace-replaces-all-values.json index 0c61e9d64..bc0e9729f 100644 --- a/types/test-cases/reducers/290-chat-configchanged-replace-replaces-all-values.json +++ b/types/test-cases/reducers/290-chat-configchanged-replace-replaces-all-values.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged with replace replaces the chat's config overlay", + "description": "chat/configChanged with replace replaces the chat's config values", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/291-chat-configchanged-creates-overlay-when-config-undefined.json b/types/test-cases/reducers/291-chat-configchanged-creates-config-when-undefined.json similarity index 85% rename from types/test-cases/reducers/291-chat-configchanged-creates-overlay-when-config-undefined.json rename to types/test-cases/reducers/291-chat-configchanged-creates-config-when-undefined.json index cbace88a0..365fa640b 100644 --- a/types/test-cases/reducers/291-chat-configchanged-creates-overlay-when-config-undefined.json +++ b/types/test-cases/reducers/291-chat-configchanged-creates-config-when-undefined.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged creates the chat's config overlay when the chat has none", + "description": "chat/configChanged creates the chat's config when the chat has none", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/292-chat-configchanged-replace-without-key-resets-to-session-value.json b/types/test-cases/reducers/292-chat-configchanged-replace-without-key-resets-to-schema-default.json similarity index 83% rename from types/test-cases/reducers/292-chat-configchanged-replace-without-key-resets-to-session-value.json rename to types/test-cases/reducers/292-chat-configchanged-replace-without-key-resets-to-schema-default.json index e83ddd3dc..f7c2af192 100644 --- a/types/test-cases/reducers/292-chat-configchanged-replace-without-key-resets-to-session-value.json +++ b/types/test-cases/reducers/292-chat-configchanged-replace-without-key-resets-to-schema-default.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged with replace and the overlay minus a key resets that key to the session's value", + "description": "chat/configChanged with replace and the values minus a key drops that key, which then takes its schema default", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/309-session-chatupdated-ignores-provider.json b/types/test-cases/reducers/316-session-chatupdated-ignores-provider.json similarity index 100% rename from types/test-cases/reducers/309-session-chatupdated-ignores-provider.json rename to types/test-cases/reducers/316-session-chatupdated-ignores-provider.json diff --git a/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json b/types/test-cases/reducers/317-chat-configchanged-replace-creates-config-when-absent.json similarity index 86% rename from types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json rename to types/test-cases/reducers/317-chat-configchanged-replace-creates-config-when-absent.json index e1f0b8502..650a2c6fb 100644 --- a/types/test-cases/reducers/311-chat-configchanged-replace-creates-config-when-absent.json +++ b/types/test-cases/reducers/317-chat-configchanged-replace-creates-config-when-absent.json @@ -1,5 +1,5 @@ { - "description": "chat/configChanged with replace creates the config overlay when it is absent", + "description": "chat/configChanged with replace creates the chat's config when it is absent", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/318-chat-configchanged-preserves-chat-schema.json b/types/test-cases/reducers/318-chat-configchanged-preserves-chat-schema.json new file mode 100644 index 000000000..9136e2af8 --- /dev/null +++ b/types/test-cases/reducers/318-chat-configchanged-preserves-chat-schema.json @@ -0,0 +1,60 @@ +{ + "description": "chat/configChanged merges into the chat's config values and preserves its config schema", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": false + } + } + }, + "actions": [ + { + "type": "chat/configChanged", + "config": { + "autoApprove": true + } + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": true + } + } + } +} diff --git a/types/test-cases/reducers/319-chat-configchanged-replace-preserves-chat-schema.json b/types/test-cases/reducers/319-chat-configchanged-replace-preserves-chat-schema.json new file mode 100644 index 000000000..6e4cb4025 --- /dev/null +++ b/types/test-cases/reducers/319-chat-configchanged-replace-preserves-chat-schema.json @@ -0,0 +1,57 @@ +{ + "description": "chat/configChanged with replace replaces the chat's config values and preserves its config schema", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": true + } + } + }, + "actions": [ + { + "type": "chat/configChanged", + "config": {}, + "replace": true + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": {} + } + } +} diff --git a/types/test-cases/round-trips/056-chat-config-snapshot.json b/types/test-cases/round-trips/056-chat-config-snapshot.json index c22144f61..452f09554 100644 --- a/types/test-cases/round-trips/056-chat-config-snapshot.json +++ b/types/test-cases/round-trips/056-chat-config-snapshot.json @@ -1,7 +1,7 @@ { "name": "chat-config-snapshot", "group": "A", - "description": "ChatState.config is an overlay that carries only the chat's own config values (no schema); it survives a decode/re-encode cycle on a chat snapshot.", + "description": "ChatState.config carries the chat's complete config values without a schema when the chat uses the session's schema; it survives a decode/re-encode cycle on a chat snapshot.", "type": "Snapshot", "input": { "resource": "ahp-chat:/peer", diff --git a/types/test-cases/round-trips/076-create-chat-provider.json b/types/test-cases/round-trips/079-create-chat-provider.json similarity index 100% rename from types/test-cases/round-trips/076-create-chat-provider.json rename to types/test-cases/round-trips/079-create-chat-provider.json diff --git a/types/test-cases/round-trips/077-initialize-multiple-chat-providers.json b/types/test-cases/round-trips/080-initialize-multiple-chat-providers.json similarity index 100% rename from types/test-cases/round-trips/077-initialize-multiple-chat-providers.json rename to types/test-cases/round-trips/080-initialize-multiple-chat-providers.json diff --git a/types/test-cases/round-trips/078-chat-summary-provider.json b/types/test-cases/round-trips/081-chat-summary-provider.json similarity index 100% rename from types/test-cases/round-trips/078-chat-summary-provider.json rename to types/test-cases/round-trips/081-chat-summary-provider.json diff --git a/types/test-cases/round-trips/080-session-chat-summary-provider-round-trip.json b/types/test-cases/round-trips/082-session-chat-summary-provider-round-trip.json similarity index 100% rename from types/test-cases/round-trips/080-session-chat-summary-provider-round-trip.json rename to types/test-cases/round-trips/082-session-chat-summary-provider-round-trip.json diff --git a/types/test-cases/round-trips/083-chat-config-schema-snapshot.json b/types/test-cases/round-trips/083-chat-config-schema-snapshot.json new file mode 100644 index 000000000..c0cb4118e --- /dev/null +++ b/types/test-cases/round-trips/083-chat-config-schema-snapshot.json @@ -0,0 +1,64 @@ +{ + "name": "chat-config-schema-snapshot", + "group": "A", + "description": "A chat snapshot preserves a per-chat config schema for a chat backed by a different agent than its session.", + "type": "Snapshot", + "input": { + "resource": "ahp-chat:/c2", + "state": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": false + } + } + }, + "fromSeq": 4 + }, + "acceptableOutputs": [ + { + "resource": "ahp-chat:/c2", + "state": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Other agent", + "status": 1, + "modifiedAt": "2026-10-19T12:00:00.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": false + } + } + }, + "fromSeq": 4 + } + ] +} From 8b6a3ffe091b3eb5b0dd30576d55dbd234872a4c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 23 Oct 2026 18:35:31 +0200 Subject: [PATCH 09/13] chat: per-chat config values cover chatMutable properties only - `ChatConfigState.values` holds the chat's `chatMutable` properties of the effective schema; other properties are session-wide. - Hosts set `config` on every chat when the session has a config. - moveChat config paragraph tightened. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Actions.generated.cs | 6 ++--- .../Generated/Commands.generated.cs | 6 ++--- .../Generated/State.generated.cs | 25 ++++++++++-------- clients/go/ahptypes/actions.generated.go | 6 ++--- clients/go/ahptypes/commands.generated.go | 6 ++--- clients/go/ahptypes/state.generated.go | 25 ++++++++++-------- .../generated/State.generated.kt | 13 +++++----- clients/rust/crates/ahp-types/src/actions.rs | 6 ++--- clients/rust/crates/ahp-types/src/commands.rs | 6 ++--- clients/rust/crates/ahp-types/src/state.rs | 25 ++++++++++-------- .../Generated/State.generated.swift | 13 +++++----- docs/.changes/20261014-chat-config.json | 2 +- docs/guide/state-model.md | 9 +++---- docs/specification/chat-channel.md | 26 +++++++++---------- schema/actions.schema.json | 8 +++--- schema/commands.schema.json | 10 +++---- schema/errors.schema.json | 10 +++---- schema/notifications.schema.json | 6 ++--- schema/state.schema.json | 6 ++--- types/channels-chat/actions.ts | 6 ++--- types/channels-chat/commands.ts | 6 ++--- types/channels-chat/state.ts | 25 ++++++++++-------- .../round-trips/056-chat-config-snapshot.json | 2 +- 23 files changed, 133 insertions(+), 120 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index f687d1e51..53307bb9b 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -2092,9 +2092,9 @@ public sealed record ChatIsArchivedChangedAction /// Config values set on a chat changed. /// /// Merges `config` into the chat's -/// {@link ChatConfigState | `ChatState.config`} values, or sets the complete -/// values when `replace` is `true`; keys missing after a replace take the -/// effective schema's `default`. Only properties with `chatMutable: true` in +/// {@link ChatConfigState | `ChatState.config`} values, or replaces them when +/// `replace` is `true`; keys missing after a replace take their property's +/// `default`. Only properties with `chatMutable: true` in /// the effective schema may be set. public sealed record ChatConfigChangedAction { diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index b84b92cf1..7a600b2b7 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -1115,9 +1115,9 @@ public sealed record ChatMoveToNewSessionDestination /// the chat's agent matches its session's. /// /// A moved chat's `config.values` move with it unchanged. If a move changes -/// whether the chat needs its own `config.schema`, the host either sets or -/// clears it, and clients SHOULD resubscribe to the chat since no action -/// carries a schema change, or refuses the move as an unsupported destination. +/// whether the chat needs its own `config.schema`, the host sets or clears it, +/// or refuses the move as an unsupported destination. Clients SHOULD +/// resubscribe to a moved chat whose `config.schema` changed. /// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index c9df09b63..11c1ddba1 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -3517,25 +3517,28 @@ public sealed class SessionConfigState public required Dictionary Values { get; set; } } -/// A chat's configuration. It does not inherit from the session: changing the -/// session's config never changes a chat's. +/// A chat's configuration. `values` holds the chat's `chatMutable` properties +/// of the effective schema; other properties are session-wide and come from +/// the session's config. It does not inherit from the session: changing the +/// session's config never changes a chat's `chatMutable` values. /// -/// Only properties with `chatMutable: true` in the effective schema can be -/// changed with `chat/configChanged`; hosts reject other keys. Hosts set the -/// initial `values` when they create the chat. +/// Only `chatMutable` properties can be changed with `chat/configChanged`; +/// hosts reject other keys. Hosts set the initial `values` when they create +/// the chat. When the session has a config, hosts set `config` on every chat. public sealed class ChatConfigState { - /// Config schema for this chat. Absent means the session's schema. Hosts set - /// it when the chat's {@link ChatState.provider | `provider`} differs from - /// the session's; when present, clients use it instead of the session's - /// schema. + /// Config schema for this chat's `chatMutable` properties. Absent means the + /// session's schema. Hosts set it, as the chat's own schema, when the chat's + /// {@link ChatState.provider | `provider`} differs from the session's; when + /// present, clients use it instead of the session's schema. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionConfigSchema? Schema { get; set; } - /// The chat's complete config, keyed by property id. A missing key takes the - /// effective schema's `default`. + /// The chat's values for the `chatMutable` properties of the effective + /// schema, keyed by property id. A missing key takes that property's + /// `default`. public required Dictionary Values { get; set; } } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 44e7b0073..e59c632af 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -877,9 +877,9 @@ type ChatIsArchivedChangedAction struct { // Config values set on a chat changed. // // Merges `config` into the chat's -// {@link ChatConfigState | `ChatState.config`} values, or sets the complete -// values when `replace` is `true`; keys missing after a replace take the -// effective schema's `default`. Only properties with `chatMutable: true` in +// {@link ChatConfigState | `ChatState.config`} values, or replaces them when +// `replace` is `true`; keys missing after a replace take their property's +// `default`. Only properties with `chatMutable: true` in // the effective schema may be set. type ChatConfigChangedAction struct { Type ActionType `json:"type"` diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 57dd0e3ed..5d66136a1 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -738,9 +738,9 @@ type ChatMoveToNewSessionDestination struct { // the chat's agent matches its session's. // // A moved chat's `config.values` move with it unchanged. If a move changes -// whether the chat needs its own `config.schema`, the host either sets or -// clears it, and clients SHOULD resubscribe to the chat since no action -// carries a schema change, or refuses the move as an unsupported destination. +// whether the chat needs its own `config.schema`, the host sets or clears it, +// or refuses the move as an unsupported destination. Clients SHOULD +// resubscribe to a moved chat whose `config.schema` changed. // // On success every moved chat keeps its URI, state, and immutable // `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 409aceb34..0406a139a 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1706,22 +1706,25 @@ type SessionConfigState struct { Values map[string]json.RawMessage `json:"values"` } -// A chat's configuration. It does not inherit from the session: changing the -// session's config never changes a chat's. +// A chat's configuration. `values` holds the chat's `chatMutable` properties +// of the effective schema; other properties are session-wide and come from +// the session's config. It does not inherit from the session: changing the +// session's config never changes a chat's `chatMutable` values. // -// Only properties with `chatMutable: true` in the effective schema can be -// changed with `chat/configChanged`; hosts reject other keys. Hosts set the -// initial `values` when they create the chat. +// Only `chatMutable` properties can be changed with `chat/configChanged`; +// hosts reject other keys. Hosts set the initial `values` when they create +// the chat. When the session has a config, hosts set `config` on every chat. type ChatConfigState struct { - // Config schema for this chat. Absent means the session's schema. Hosts set - // it when the chat's {@link ChatState.provider | `provider`} differs from - // the session's; when present, clients use it instead of the session's - // schema. + // Config schema for this chat's `chatMutable` properties. Absent means the + // session's schema. Hosts set it, as the chat's own schema, when the chat's + // {@link ChatState.provider | `provider`} differs from the session's; when + // present, clients use it instead of the session's schema. // // Stability: 1.0 - Early development. Schema *SessionConfigSchema `json:"schema,omitempty"` - // The chat's complete config, keyed by property id. A missing key takes the - // effective schema's `default`. + // The chat's values for the `chatMutable` properties of the effective + // schema, keyed by property id. A missing key takes that property's + // `default`. Values map[string]json.RawMessage `json:"values"` } 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 78f0b4f9d..80bbdf6d6 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 @@ -2568,17 +2568,18 @@ data class SessionConfigState( @Serializable data class ChatConfigState( /** - * Config schema for this chat. Absent means the session's schema. Hosts set - * it when the chat's {@link ChatState.provider | `provider`} differs from - * the session's; when present, clients use it instead of the session's - * schema. + * Config schema for this chat's `chatMutable` properties. Absent means the + * session's schema. Hosts set it, as the chat's own schema, when the chat's + * {@link ChatState.provider | `provider`} differs from the session's; when + * present, clients use it instead of the session's schema. * * Stability: 1.0 - Early development. */ val schema: SessionConfigSchema? = null, /** - * The chat's complete config, keyed by property id. A missing key takes the - * effective schema's `default`. + * The chat's values for the `chatMutable` properties of the effective + * schema, keyed by property id. A missing key takes that property's + * `default`. */ val values: Map ) diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index ad3f7abb9..8d25d5dbf 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1554,9 +1554,9 @@ pub struct ChatIsArchivedChangedAction { /// Config values set on a chat changed. /// /// Merges `config` into the chat's -/// {@link ChatConfigState | `ChatState.config`} values, or sets the complete -/// values when `replace` is `true`; keys missing after a replace take the -/// effective schema's `default`. Only properties with `chatMutable: true` in +/// {@link ChatConfigState | `ChatState.config`} values, or replaces them when +/// `replace` is `true`; keys missing after a replace take their property's +/// `default`. Only properties with `chatMutable: true` in /// the effective schema may be set. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index adcb779e6..ca227dc3b 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -1024,9 +1024,9 @@ pub struct ChatMoveToNewSessionDestination {} /// the chat's agent matches its session's. /// /// A moved chat's `config.values` move with it unchanged. If a move changes -/// whether the chat needs its own `config.schema`, the host either sets or -/// clears it, and clients SHOULD resubscribe to the chat since no action -/// carries a schema change, or refuses the move as an unsupported destination. +/// whether the chat needs its own `config.schema`, the host sets or clears it, +/// or refuses the move as an unsupported destination. Clients SHOULD +/// resubscribe to a moved chat whose `config.schema` changed. /// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 498a34765..7d5bd4cd4 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2889,25 +2889,28 @@ pub struct SessionConfigState { pub values: JsonObject, } -/// A chat's configuration. It does not inherit from the session: changing the -/// session's config never changes a chat's. +/// A chat's configuration. `values` holds the chat's `chatMutable` properties +/// of the effective schema; other properties are session-wide and come from +/// the session's config. It does not inherit from the session: changing the +/// session's config never changes a chat's `chatMutable` values. /// -/// Only properties with `chatMutable: true` in the effective schema can be -/// changed with `chat/configChanged`; hosts reject other keys. Hosts set the -/// initial `values` when they create the chat. +/// Only `chatMutable` properties can be changed with `chat/configChanged`; +/// hosts reject other keys. Hosts set the initial `values` when they create +/// the chat. When the session has a config, hosts set `config` on every chat. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatConfigState { - /// Config schema for this chat. Absent means the session's schema. Hosts set - /// it when the chat's {@link ChatState.provider | `provider`} differs from - /// the session's; when present, clients use it instead of the session's - /// schema. + /// Config schema for this chat's `chatMutable` properties. Absent means the + /// session's schema. Hosts set it, as the chat's own schema, when the chat's + /// {@link ChatState.provider | `provider`} differs from the session's; when + /// present, clients use it instead of the session's schema. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, - /// The chat's complete config, keyed by property id. A missing key takes the - /// effective schema's `default`. + /// The chat's values for the `chatMutable` properties of the effective + /// schema, keyed by property id. A missing key takes that property's + /// `default`. pub values: JsonObject, } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 2c19a5b62..5fd1ae686 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -2691,15 +2691,16 @@ public struct SessionConfigState: Codable, Sendable { } public struct ChatConfigState: Codable, Sendable { - /// Config schema for this chat. Absent means the session's schema. Hosts set - /// it when the chat's {@link ChatState.provider | `provider`} differs from - /// the session's; when present, clients use it instead of the session's - /// schema. + /// Config schema for this chat's `chatMutable` properties. Absent means the + /// session's schema. Hosts set it, as the chat's own schema, when the chat's + /// {@link ChatState.provider | `provider`} differs from the session's; when + /// present, clients use it instead of the session's schema. /// /// Stability: 1.0 - Early development. public var schema: SessionConfigSchema? - /// The chat's complete config, keyed by property id. A missing key takes the - /// effective schema's `default`. + /// The chat's values for the `chatMutable` properties of the effective + /// schema, keyed by property id. A missing key takes that property's + /// `default`. public var values: [String: AnyCodable] public init( diff --git a/docs/.changes/20261014-chat-config.json b/docs/.changes/20261014-chat-config.json index a18c507ca..5cc418568 100644 --- a/docs/.changes/20261014-chat-config.json +++ b/docs/.changes/20261014-chat-config.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "`ChatState.config`, the `chat/configChanged` action, and `chatMutable` on session config properties give each chat its own config for selected settings, such as tool auto-approval, so changing them in one chat no longer changes every chat in the session. `ChatConfigState.values` is the chat's complete config and does not inherit from the session; missing keys take the schema default. Optional `ChatConfigState.schema` replaces the session's schema for a chat whose agent differs from the session's." + "message": "`ChatState.config`, the `chat/configChanged` action, and `chatMutable` on session config properties give each chat its own config for selected settings, such as tool auto-approval, so changing them in one chat no longer changes every chat in the session. `ChatConfigState.values` holds the chat's `chatMutable` properties and does not inherit from the session; missing keys take the schema default. Optional `ChatConfigState.schema` replaces the session's schema for a chat whose agent differs from the session's." } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 82eb45856..11eccd26f 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -195,7 +195,7 @@ ChatState { provider?: string // chat's agent; absent = session's provider workingDirectories?: URI[] // subset of session's workingDirectories changesets?: Changeset[] // per-chat Branch, Uncommitted Changes, etc. - config?: ChatConfigState // chat's complete config (+ optional schema) + config?: ChatConfigState // chat's chatMutable values (+ optional schema) turns: Turn[] // completed turns turnsNextCursor?: string // page older turns via fetchTurns @@ -213,10 +213,9 @@ changeset channel contract. Hosts scope a chat's catalogue to its effective working directories: the chat's subset when present, otherwise the full session set. -`config.values` is the chat's complete config; it does not inherit from the -session. `config.schema` is present only when the chat's agent differs from the -session's; otherwise the session's schema applies. Only `chatMutable` -properties can be changed, with `chat/configChanged`. See +`config.values` holds the chat's own values for its `chatMutable` settings; +`config.schema` is present only when the chat's agent differs from the +session's. See [Per-chat configuration](/specification/chat-channel#per-chat-configuration). `provider` names the chat's agent when it differs from the session's. Hosts diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 97faebca4..eb921ee48 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -78,19 +78,19 @@ fully replaces the catalogue or clears it when `changesets` is absent. ### Per-chat configuration -[`ChatState.config`](/reference/chat#chatstate) is the chat's own -configuration. `values` is always its complete config, set by the host when it -creates the chat; it does not inherit from the session, so changing the -session's config never changes a chat's. A key missing from `values` takes the -effective schema's `default`. +[`ChatState.config`](/reference/chat#chatstate) holds the chat's own values for +the [`chatMutable`](/reference/session#sessionconfigpropertyschema) properties +of its effective schema; other properties are session-wide and come from the +session's config. The host sets the initial `values` when it creates the chat, +and when the session has a config, it sets `config` on every chat. Changing the +session's config never changes a chat's `chatMutable` values. A key missing +from `values` takes that property's `default`. The effective schema is `config.schema` when present, otherwise the session's -schema. Hosts set `config.schema` only for a chat whose `provider` differs from -the session's. +schema. Hosts set `config.schema`, the chat's own schema for its `chatMutable` +properties, only for a chat whose `provider` differs from the session's. -Clients change a chat's -[`chatMutable`](/reference/session#sessionconfigpropertyschema) properties by -dispatching [`chat/configChanged`](/reference/chat#chatconfigchangedaction), +Clients change a chat's `chatMutable` properties by dispatching [`chat/configChanged`](/reference/chat#chatconfigchangedaction), which merges into `values` or replaces them when `replace` is `true`. Hosts reject keys that are not `chatMutable` in the effective schema. @@ -256,9 +256,9 @@ when the chat had none because it matched its old session. Hosts MAY refuse, as an unsupported destination, a move they cannot support. A moved chat's `config.values` move with it unchanged. If a move changes whether -the chat needs its own `config.schema`, the host either sets or clears it -(clients SHOULD resubscribe, since no action carries a schema change) or -refuses the move as an unsupported destination. +the chat needs its own `config.schema`, the host sets or clears it, or refuses +the move as an unsupported destination. Clients SHOULD resubscribe to a moved +chat whose `config.schema` changed. #### Atomic synchronization diff --git a/schema/actions.schema.json b/schema/actions.schema.json index ad52a534f..d667ee2f4 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -1714,7 +1714,7 @@ }, "ChatConfigChangedAction": { "type": "object", - "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or sets the complete\nvalues when `replace` is `true`; keys missing after a replace take the\neffective schema's `default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", + "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or replaces them when\n`replace` is `true`; keys missing after a replace take their property's\n`default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", "properties": { "type": { "const": "chat/configChanged" @@ -5492,16 +5492,16 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat's `chatMutable` properties. Absent means the\nsession's schema. Hosts set it, as the chat's own schema, when the chat's\n{@link ChatState.provider | `provider`} differs from the session's; when\npresent, clients use it instead of the session's schema.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." + "description": "The chat's values for the `chatMutable` properties of the effective\nschema, keyed by property id. A missing key takes that property's\n`default`." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 2eb1dddcf..9c712bca0 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1509,7 +1509,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host either sets or\nclears it, and clients SHOULD resubscribe to the chat since no action\ncarries a schema change, or refuses the move as an unsupported destination.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host sets or clears it,\nor refuses the move as an unsupported destination. Clients SHOULD\nresubscribe to a moved chat whose `config.schema` changed.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -4813,16 +4813,16 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat's `chatMutable` properties. Absent means the\nsession's schema. Hosts set it, as the chat's own schema, when the chat's\n{@link ChatState.provider | `provider`} differs from the session's; when\npresent, clients use it instead of the session's schema.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." + "description": "The chat's values for the `chatMutable` properties of the effective\nschema, keyed by property id. A missing key takes that property's\n`default`." } }, "required": [ @@ -9735,7 +9735,7 @@ }, "ChatConfigChangedAction": { "type": "object", - "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or sets the complete\nvalues when `replace` is `true`; keys missing after a replace take the\neffective schema's `default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", + "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or replaces them when\n`replace` is `true`; keys missing after a replace take their property's\n`default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", "properties": { "type": { "const": "chat/configChanged" diff --git a/schema/errors.schema.json b/schema/errors.schema.json index a54c1b49b..a3ca280b7 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2902,16 +2902,16 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat's `chatMutable` properties. Absent means the\nsession's schema. Hosts set it, as the chat's own schema, when the chat's\n{@link ChatState.provider | `provider`} differs from the session's; when\npresent, clients use it instead of the session's schema.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." + "description": "The chat's values for the `chatMutable` properties of the effective\nschema, keyed by property id. A missing key takes that property's\n`default`." } }, "required": [ @@ -7619,7 +7619,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host either sets or\nclears it, and clients SHOULD resubscribe to the chat since no action\ncarries a schema change, or refuses the move as an unsupported destination.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host sets or clears it,\nor refuses the move as an unsupported destination. Clients SHOULD\nresubscribe to a moved chat whose `config.schema` changed.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -10791,7 +10791,7 @@ }, "ChatConfigChangedAction": { "type": "object", - "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or sets the complete\nvalues when `replace` is `true`; keys missing after a replace take the\neffective schema's `default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", + "description": "Config values set on a chat changed.\n\nMerges `config` into the chat's\n{@link ChatConfigState | `ChatState.config`} values, or replaces them when\n`replace` is `true`; keys missing after a replace take their property's\n`default`. Only properties with `chatMutable: true` in\nthe effective schema may be set.", "properties": { "type": { "const": "chat/configChanged" diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 060bee4ed..5302d7755 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3204,16 +3204,16 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat's `chatMutable` properties. Absent means the\nsession's schema. Hosts set it, as the chat's own schema, when the chat's\n{@link ChatState.provider | `provider`} differs from the session's; when\npresent, clients use it instead of the session's schema.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." + "description": "The chat's values for the `chatMutable` properties of the effective\nschema, keyed by property id. A missing key takes that property's\n`default`." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index ca5f22314..70377263a 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2813,16 +2813,16 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. It does not inherit from the session: changing the\nsession's config never changes a chat's.\n\nOnly properties with `chatMutable: true` in the effective schema can be\nchanged with `chat/configChanged`; hosts reject other keys. Hosts set the\ninitial `values` when they create the chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", - "description": "Config schema for this chat. Absent means the session's schema. Hosts set\nit when the chat's {@link ChatState.provider | `provider`} differs from\nthe session's; when present, clients use it instead of the session's\nschema.\n\nStability: 1.0 - Early development." + "description": "Config schema for this chat's `chatMutable` properties. Absent means the\nsession's schema. Hosts set it, as the chat's own schema, when the chat's\n{@link ChatState.provider | `provider`} differs from the session's; when\npresent, clients use it instead of the session's schema.\n\nStability: 1.0 - Early development." }, "values": { "type": "object", "additionalProperties": {}, - "description": "The chat's complete config, keyed by property id. A missing key takes the\neffective schema's `default`." + "description": "The chat's values for the `chatMutable` properties of the effective\nschema, keyed by property id. A missing key takes that property's\n`default`." } }, "required": [ diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index 75733304d..cd2754057 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -905,9 +905,9 @@ export interface ChatIsArchivedChangedAction { * Config values set on a chat changed. * * Merges `config` into the chat's - * {@link ChatConfigState | `ChatState.config`} values, or sets the complete - * values when `replace` is `true`; keys missing after a replace take the - * effective schema's `default`. Only properties with `chatMutable: true` in + * {@link ChatConfigState | `ChatState.config`} values, or replaces them when + * `replace` is `true`; keys missing after a replace take their property's + * `default`. Only properties with `chatMutable: true` in * the effective schema may be set. * * @category Chat Actions diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 585e7517d..ac3daa946 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -209,9 +209,9 @@ export type ChatMoveDestination = * the chat's agent matches its session's. * * A moved chat's `config.values` move with it unchanged. If a move changes - * whether the chat needs its own `config.schema`, the host either sets or - * clears it, and clients SHOULD resubscribe to the chat since no action - * carries a schema change, or refuses the move as an unsupported destination. + * whether the chat needs its own `config.schema`, the host sets or clears it, + * or refuses the move as an unsupported destination. Clients SHOULD + * resubscribe to a moved chat whose `config.schema` changed. * * On success every moved chat keeps its URI, state, and immutable * `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 20ff5def1..d2691b251 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -430,27 +430,30 @@ export const enum ChatInteractivity { // ─── Chat Config Types ─────────────────────────────────────────────────────── /** - * A chat's configuration. It does not inherit from the session: changing the - * session's config never changes a chat's. + * A chat's configuration. `values` holds the chat's `chatMutable` properties + * of the effective schema; other properties are session-wide and come from + * the session's config. It does not inherit from the session: changing the + * session's config never changes a chat's `chatMutable` values. * - * Only properties with `chatMutable: true` in the effective schema can be - * changed with `chat/configChanged`; hosts reject other keys. Hosts set the - * initial `values` when they create the chat. + * Only `chatMutable` properties can be changed with `chat/configChanged`; + * hosts reject other keys. Hosts set the initial `values` when they create + * the chat. When the session has a config, hosts set `config` on every chat. * * @category Chat Config Types */ export interface ChatConfigState { /** - * Config schema for this chat. Absent means the session's schema. Hosts set - * it when the chat's {@link ChatState.provider | `provider`} differs from - * the session's; when present, clients use it instead of the session's - * schema. + * Config schema for this chat's `chatMutable` properties. Absent means the + * session's schema. Hosts set it, as the chat's own schema, when the chat's + * {@link ChatState.provider | `provider`} differs from the session's; when + * present, clients use it instead of the session's schema. * @stability 1.0 */ schema?: SessionConfigSchema; /** - * The chat's complete config, keyed by property id. A missing key takes the - * effective schema's `default`. + * The chat's values for the `chatMutable` properties of the effective + * schema, keyed by property id. A missing key takes that property's + * `default`. */ values: Record; } diff --git a/types/test-cases/round-trips/056-chat-config-snapshot.json b/types/test-cases/round-trips/056-chat-config-snapshot.json index 452f09554..8e7a95a0b 100644 --- a/types/test-cases/round-trips/056-chat-config-snapshot.json +++ b/types/test-cases/round-trips/056-chat-config-snapshot.json @@ -1,7 +1,7 @@ { "name": "chat-config-snapshot", "group": "A", - "description": "ChatState.config carries the chat's complete config values without a schema when the chat uses the session's schema; it survives a decode/re-encode cycle on a chat snapshot.", + "description": "ChatState.config carries the chat's chatMutable values without a schema when the chat uses the session's schema; it survives a decode/re-encode cycle on a chat snapshot.", "type": "Snapshot", "input": { "resource": "ahp-chat:/peer", From eab1ec33ea7bbb5f28ec46d006f0bc163fa3cb37 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 23 Oct 2026 18:38:11 +0200 Subject: [PATCH 10/13] docs: chat config is no longer an overlay Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/specification/chat-channel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index eb921ee48..d6d35353e 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -21,7 +21,7 @@ Multiple chat channels may be active simultaneously. Clients subscribe to each c ## State -Subscribers receive a [`ChatState`](/reference/chat#chatstate) snapshot. `ChatState` denormalizes the [`ChatSummary`](/reference/chat#chatsummary) fields directly onto itself (`resource`, `title`, `status`, `activity`, `modifiedAt`, `origin`, `workingDirectories`) and adds the conversation contents (history of completed turns, the active turn if any, pending messages, outstanding input requests, and the user's in-progress [`draft`](#drafts)) plus the optional [`changesets`](#per-chat-changesets) catalogue and the chat's [`config`](#per-chat-configuration) overlay. Producers MUST keep the chat's `ChatSummary` in the session catalog consistent with these inlined summary fields — typically by dispatching a matching [`session/chatUpdated`](/reference/session#actions) whenever any summary field on the chat changes. Refer to the [State Model guide](/guide/state-model) for a structural overview. +Subscribers receive a [`ChatState`](/reference/chat#chatstate) snapshot. `ChatState` denormalizes the [`ChatSummary`](/reference/chat#chatsummary) fields directly onto itself (`resource`, `title`, `status`, `activity`, `modifiedAt`, `origin`, `workingDirectories`) and adds the conversation contents (history of completed turns, the active turn if any, pending messages, outstanding input requests, and the user's in-progress [`draft`](#drafts)) plus the optional [`changesets`](#per-chat-changesets) catalogue and the chat's own [`config`](#per-chat-configuration). Producers MUST keep the chat's `ChatSummary` in the session catalog consistent with these inlined summary fields — typically by dispatching a matching [`session/chatUpdated`](/reference/session#actions) whenever any summary field on the chat changes. Refer to the [State Model guide](/guide/state-model) for a structural overview. Clients mark any known chat, including the owning session's default chat, as read or unread by dispatching From d127fa02c66ecefef1462739d0bbb916a3d3f933 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 23 Oct 2026 19:03:14 +0200 Subject: [PATCH 11/13] chat: a chat with its own schema doesn't use the session's config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/State.generated.cs | 8 +++++--- clients/go/ahptypes/state.generated.go | 8 +++++--- clients/rust/crates/ahp-types/src/state.rs | 8 +++++--- docs/specification/chat-channel.md | 5 +++-- schema/actions.schema.json | 2 +- schema/commands.schema.json | 2 +- schema/errors.schema.json | 2 +- schema/notifications.schema.json | 2 +- schema/state.schema.json | 2 +- types/channels-chat/state.ts | 8 +++++--- 10 files changed, 28 insertions(+), 19 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 11c1ddba1..54eb931e8 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -3518,9 +3518,11 @@ public sealed class SessionConfigState } /// A chat's configuration. `values` holds the chat's `chatMutable` properties -/// of the effective schema; other properties are session-wide and come from -/// the session's config. It does not inherit from the session: changing the -/// session's config never changes a chat's `chatMutable` values. +/// of the effective schema. For a chat without its own `schema`, other +/// properties are session-wide and come from the session's config; a chat with +/// its own `schema` does not use the session's config. It does not inherit +/// from the session: changing the session's config never changes a chat's +/// `chatMutable` values. /// /// Only `chatMutable` properties can be changed with `chat/configChanged`; /// hosts reject other keys. Hosts set the initial `values` when they create diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 0406a139a..e9358c3d3 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1707,9 +1707,11 @@ type SessionConfigState struct { } // A chat's configuration. `values` holds the chat's `chatMutable` properties -// of the effective schema; other properties are session-wide and come from -// the session's config. It does not inherit from the session: changing the -// session's config never changes a chat's `chatMutable` values. +// of the effective schema. For a chat without its own `schema`, other +// properties are session-wide and come from the session's config; a chat with +// its own `schema` does not use the session's config. It does not inherit +// from the session: changing the session's config never changes a chat's +// `chatMutable` values. // // Only `chatMutable` properties can be changed with `chat/configChanged`; // hosts reject other keys. Hosts set the initial `values` when they create diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 7d5bd4cd4..7925461ad 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2890,9 +2890,11 @@ pub struct SessionConfigState { } /// A chat's configuration. `values` holds the chat's `chatMutable` properties -/// of the effective schema; other properties are session-wide and come from -/// the session's config. It does not inherit from the session: changing the -/// session's config never changes a chat's `chatMutable` values. +/// of the effective schema. For a chat without its own `schema`, other +/// properties are session-wide and come from the session's config; a chat with +/// its own `schema` does not use the session's config. It does not inherit +/// from the session: changing the session's config never changes a chat's +/// `chatMutable` values. /// /// Only `chatMutable` properties can be changed with `chat/configChanged`; /// hosts reject other keys. Hosts set the initial `values` when they create diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index d6d35353e..013faea68 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -80,8 +80,9 @@ fully replaces the catalogue or clears it when `changesets` is absent. [`ChatState.config`](/reference/chat#chatstate) holds the chat's own values for the [`chatMutable`](/reference/session#sessionconfigpropertyschema) properties -of its effective schema; other properties are session-wide and come from the -session's config. The host sets the initial `values` when it creates the chat, +of its effective schema. For a chat without its own `config.schema`, other +properties are session-wide and come from the session's config; a chat with its +own `config.schema` does not use the session's config. The host sets the initial `values` when it creates the chat, and when the session has a config, it sets `config` on every chat. Changing the session's config never changes a chat's `chatMutable` values. A key missing from `values` takes that property's `default`. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index d667ee2f4..ef5ea64c8 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5492,7 +5492,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 9c712bca0..1e47978ef 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4813,7 +4813,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index a3ca280b7..f4d7c7ee5 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2902,7 +2902,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 5302d7755..efac67b0f 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3204,7 +3204,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/state.schema.json b/schema/state.schema.json index 70377263a..e7fedc36c 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2813,7 +2813,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema; other properties are session-wide and come from\nthe session's config. It does not inherit from the session: changing the\nsession's config never changes a chat's `chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index d2691b251..8785264bf 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -431,9 +431,11 @@ export const enum ChatInteractivity { /** * A chat's configuration. `values` holds the chat's `chatMutable` properties - * of the effective schema; other properties are session-wide and come from - * the session's config. It does not inherit from the session: changing the - * session's config never changes a chat's `chatMutable` values. + * of the effective schema. For a chat without its own `schema`, other + * properties are session-wide and come from the session's config; a chat with + * its own `schema` does not use the session's config. It does not inherit + * from the session: changing the session's config never changes a chat's + * `chatMutable` values. * * Only `chatMutable` properties can be changed with `chat/configChanged`; * hosts reject other keys. Hosts set the initial `values` when they create From f8067f73829dd8bbd37679ba2ddacf3df0487e4d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 23 Oct 2026 19:10:42 +0200 Subject: [PATCH 12/13] chat: a chat's config values change only through chat/configChanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/State.generated.cs | 4 ++-- clients/go/ahptypes/state.generated.go | 4 ++-- clients/rust/crates/ahp-types/src/state.rs | 4 ++-- docs/specification/chat-channel.md | 5 +++-- schema/actions.schema.json | 2 +- schema/commands.schema.json | 2 +- schema/errors.schema.json | 2 +- schema/notifications.schema.json | 2 +- schema/state.schema.json | 2 +- types/channels-chat/state.ts | 4 ++-- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 54eb931e8..b0e31834b 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -3521,8 +3521,8 @@ public sealed class SessionConfigState /// of the effective schema. For a chat without its own `schema`, other /// properties are session-wide and come from the session's config; a chat with /// its own `schema` does not use the session's config. It does not inherit -/// from the session: changing the session's config never changes a chat's -/// `chatMutable` values. +/// from the session: a chat's `values` change only through +/// `chat/configChanged` on the chat. /// /// Only `chatMutable` properties can be changed with `chat/configChanged`; /// hosts reject other keys. Hosts set the initial `values` when they create diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index e9358c3d3..ae744d8f7 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1710,8 +1710,8 @@ type SessionConfigState struct { // of the effective schema. For a chat without its own `schema`, other // properties are session-wide and come from the session's config; a chat with // its own `schema` does not use the session's config. It does not inherit -// from the session: changing the session's config never changes a chat's -// `chatMutable` values. +// from the session: a chat's `values` change only through +// `chat/configChanged` on the chat. // // Only `chatMutable` properties can be changed with `chat/configChanged`; // hosts reject other keys. Hosts set the initial `values` when they create diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 7925461ad..f5bf755d6 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2893,8 +2893,8 @@ pub struct SessionConfigState { /// of the effective schema. For a chat without its own `schema`, other /// properties are session-wide and come from the session's config; a chat with /// its own `schema` does not use the session's config. It does not inherit -/// from the session: changing the session's config never changes a chat's -/// `chatMutable` values. +/// from the session: a chat's `values` change only through +/// `chat/configChanged` on the chat. /// /// Only `chatMutable` properties can be changed with `chat/configChanged`; /// hosts reject other keys. Hosts set the initial `values` when they create diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 013faea68..88b00a98d 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -83,8 +83,9 @@ the [`chatMutable`](/reference/session#sessionconfigpropertyschema) properties of its effective schema. For a chat without its own `config.schema`, other properties are session-wide and come from the session's config; a chat with its own `config.schema` does not use the session's config. The host sets the initial `values` when it creates the chat, -and when the session has a config, it sets `config` on every chat. Changing the -session's config never changes a chat's `chatMutable` values. A key missing +and when the session has a config, it sets `config` on every chat. A chat's +`values` change only through `chat/configChanged` on the chat; they do not +inherit from the session. A key missing from `values` takes that property's `default`. The effective schema is `config.schema` when present, otherwise the session's diff --git a/schema/actions.schema.json b/schema/actions.schema.json index ef5ea64c8..6e064410e 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5492,7 +5492,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: a chat's `values` change only through\n`chat/configChanged` on the chat.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 1e47978ef..e3ccdcd9e 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4813,7 +4813,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: a chat's `values` change only through\n`chat/configChanged` on the chat.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index f4d7c7ee5..2d9d56298 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2902,7 +2902,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: a chat's `values` change only through\n`chat/configChanged` on the chat.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index efac67b0f..27e572b97 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3204,7 +3204,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: a chat's `values` change only through\n`chat/configChanged` on the chat.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/schema/state.schema.json b/schema/state.schema.json index e7fedc36c..c0b011a5e 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2813,7 +2813,7 @@ }, "ChatConfigState": { "type": "object", - "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: changing the session's config never changes a chat's\n`chatMutable` values.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", + "description": "A chat's configuration. `values` holds the chat's `chatMutable` properties\nof the effective schema. For a chat without its own `schema`, other\nproperties are session-wide and come from the session's config; a chat with\nits own `schema` does not use the session's config. It does not inherit\nfrom the session: a chat's `values` change only through\n`chat/configChanged` on the chat.\n\nOnly `chatMutable` properties can be changed with `chat/configChanged`;\nhosts reject other keys. Hosts set the initial `values` when they create\nthe chat. When the session has a config, hosts set `config` on every chat.", "properties": { "schema": { "$ref": "#/$defs/SessionConfigSchema", diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 8785264bf..9fc00c6e0 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -434,8 +434,8 @@ export const enum ChatInteractivity { * of the effective schema. For a chat without its own `schema`, other * properties are session-wide and come from the session's config; a chat with * its own `schema` does not use the session's config. It does not inherit - * from the session: changing the session's config never changes a chat's - * `chatMutable` values. + * from the session: a chat's `values` change only through + * `chat/configChanged` on the chat. * * Only `chatMutable` properties can be changed with `chat/configChanged`; * hosts reject other keys. Hosts set the initial `values` when they create From 382fd311cde58d82e0eec566aca32f5728fe96e0 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 26 Oct 2026 22:10:45 +0100 Subject: [PATCH 13/13] chat: add chat/moved action After a cross-session `moveChat`, the host dispatches `chat/moved` on the chat's channel with its new session, `provider` and `config.schema` (absent clears them), so subscribers see a chat's provider and schema relative to its new session without resubscribing. - ChatMovedAction (server-originated, protocol 1.1.0) and the chat reducer in TS, Go, Rust, Kotlin, Swift and .NET. - moveChat, ChatState.provider, chat-channel.md and actions.md updated. - Reducer fixtures 320-322, round-trip 084, change fragment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Actions.generated.cs | 28 +++++++++ .../Generated/Commands.generated.cs | 5 +- .../JsonSerializerContext.generated.cs | 1 + .../Generated/State.generated.cs | 7 ++- .../Generated/ActionMetadata.generated.cs | 3 + .../dotnet/src/AgentHostProtocol/Reducers.cs | 11 ++++ clients/go/ahp/reducers.go | 8 +++ clients/go/ahptypes/actions.generated.go | 27 +++++++++ clients/go/ahptypes/commands.generated.go | 5 +- clients/go/ahptypes/state.generated.go | 7 ++- .../microsoft/agenthostprotocol/Reducers.kt | 13 +++++ .../generated/Actions.generated.kt | 21 +++++++ .../generated/State.generated.kt | 7 ++- clients/rust/crates/ahp-types/src/actions.rs | 35 +++++++++-- clients/rust/crates/ahp-types/src/commands.rs | 5 +- clients/rust/crates/ahp-types/src/state.rs | 7 ++- clients/rust/crates/ahp/src/reducers.rs | 14 +++++ .../Generated/Actions.generated.swift | 29 ++++++++++ .../Generated/State.generated.swift | 7 ++- .../Sources/AgentHostProtocol/Reducers.swift | 11 ++++ docs/.changes/20261026-chat-moved.json | 4 ++ docs/guide/actions.md | 1 + docs/specification/chat-channel.md | 8 ++- schema/actions.schema.json | 33 ++++++++++- schema/commands.schema.json | 32 +++++++++- schema/errors.schema.json | 32 +++++++++- schema/notifications.schema.json | 2 +- schema/state.schema.json | 2 +- scripts/generate-csharp.ts | 1 + scripts/generate-go.ts | 1 + scripts/generate-kotlin.ts | 1 + scripts/generate-rust.ts | 3 +- scripts/generate-swift.ts | 1 + types/action-origin.generated.ts | 4 ++ types/channels-chat/actions.ts | 27 ++++++++- types/channels-chat/commands.ts | 5 +- types/channels-chat/reducer.ts | 12 ++++ types/channels-chat/state.ts | 7 ++- types/common/actions.ts | 3 + ...0-chat-moved-sets-provider-and-schema.json | 58 +++++++++++++++++++ ...chat-moved-clears-provider-and-schema.json | 46 +++++++++++++++ ...-chat-moved-creates-config-for-schema.json | 51 ++++++++++++++++ .../round-trips/084-chat-moved-action.json | 40 +++++++++++++ types/version/registry.ts | 1 + 44 files changed, 584 insertions(+), 42 deletions(-) create mode 100644 docs/.changes/20261026-chat-moved.json create mode 100644 types/test-cases/reducers/320-chat-moved-sets-provider-and-schema.json create mode 100644 types/test-cases/reducers/321-chat-moved-clears-provider-and-schema.json create mode 100644 types/test-cases/reducers/322-chat-moved-creates-config-for-schema.json create mode 100644 types/test-cases/round-trips/084-chat-moved-action.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index 53307bb9b..cc2719067 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -129,6 +129,8 @@ public ActionType(string value) public static readonly ActionType ChatConfigChanged = new ActionType("chat/configChanged"); + public static readonly ActionType ChatMoved = new ActionType("chat/moved"); + public static readonly ActionType ChatInputRequested = new ActionType("chat/inputRequested"); public static readonly ActionType ChatInputAnswerChanged = new ActionType("chat/inputAnswerChanged"); @@ -2108,6 +2110,31 @@ public sealed record ChatConfigChangedAction public bool? Replace { get; init; } } +/// The chat moved to another session. +/// +/// Dispatched by the host on the moved chat's channel after a cross-session +/// `moveChat`. `provider` and `schema` are the chat's +/// {@link ChatState.provider | `provider`} and +/// {@link ChatConfigState.schema | `config.schema`} relative to the new +/// session; absent clears them. `ChatState` has no session field, so the +/// reducer does not store `session`; it tells subscribers which session now +/// owns the chat. +public sealed record ChatMovedAction +{ + public ActionType Type { get; init; } = ActionType.ChatMoved; + + /// The new owning session URI. + public required string Session { get; init; } + + /// The chat's provider in the new session; absent clears it. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Provider { get; init; } + + /// The chat's config schema in the new session; absent clears it. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionConfigSchema? Schema { get; init; } +} + /// A session requested input from the user. /// /// Creates an unresolved {@link InputRequestResponsePart} in the active turn, @@ -2903,6 +2930,7 @@ public StateActionConverter() ["chat/isReadChanged"] = typeof(ChatIsReadChangedAction), ["chat/isArchivedChanged"] = typeof(ChatIsArchivedChangedAction), ["chat/configChanged"] = typeof(ChatConfigChangedAction), + ["chat/moved"] = typeof(ChatMovedAction), ["chat/inputRequested"] = typeof(ChatInputRequestedAction), ["chat/inputAnswerChanged"] = typeof(ChatInputAnswerChangedAction), ["chat/inputCompleted"] = typeof(ChatInputCompletedAction), diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 7a600b2b7..6151529b0 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -1116,8 +1116,9 @@ public sealed record ChatMoveToNewSessionDestination /// /// A moved chat's `config.values` move with it unchanged. If a move changes /// whether the chat needs its own `config.schema`, the host sets or clears it, -/// or refuses the move as an unsupported destination. Clients SHOULD -/// resubscribe to a moved chat whose `config.schema` changed. +/// or refuses the move as an unsupported destination. After a cross-session +/// move the host dispatches `chat/moved` on the chat's channel with its new +/// session, `provider` and `config.schema`. /// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 1e7af38cc..b9d54208f 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -154,6 +154,7 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(ChatIsArchivedChangedAction))] [JsonSerializable(typeof(ChatIsReadChangedAction))] [JsonSerializable(typeof(ChatMovableChangedAction))] +[JsonSerializable(typeof(ChatMovedAction))] [JsonSerializable(typeof(ChatMoveDestination))] [JsonSerializable(typeof(ChatMoveDestinationKind))] [JsonSerializable(typeof(ChatMoveToNewSessionDestination))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index b0e31834b..f1341c718 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2500,9 +2500,10 @@ public sealed class ChatState /// Agent provider ID backing this chat. Absent means the session's /// {@link SessionMetadata.provider | `provider`}; hosts set it when it - /// differs. It never changes: like `resource`, it is an identity field and is - /// omitted from `session/chatUpdated` changes. Forks and side chats use the - /// source chat's agent. + /// differs. The chat's agent never changes, but `provider` is reported + /// relative to its session, so `chat/moved` may set or clear it. Like + /// `resource`, it is omitted from `session/chatUpdated` changes. Forks and + /// side chats use the source chat's agent. /// /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs b/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs index dfc79f58f..1cc062b11 100644 --- a/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs @@ -127,6 +127,9 @@ public static bool TryGetActionType(object action, out ActionType actionType) case ChatMovableChangedAction value: actionType = value.Type; return true; + case ChatMovedAction value: + actionType = value.Type; + return true; case ChatPendingMessageRemovedAction value: actionType = value.Type; return true; diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs index 979b31990..1571c7b86 100644 --- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs +++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs @@ -1144,6 +1144,17 @@ public static ReduceOutcome ApplyToChat(ChatState state, StateAction action) state.Config ??= new ChatConfigState { Values = chatValues }; state.Config.Values = chatValues; return ReduceOutcome.Applied; + case ChatMovedAction a: + state.Provider = a.Provider; + if (state.Config is not null) + { + state.Config.Schema = a.Schema; + } + else if (a.Schema is not null) + { + state.Config = new ChatConfigState { Schema = a.Schema, Values = new Dictionary() }; + } + return ReduceOutcome.Applied; } return ReduceOutcome.OutOfScope; diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index a54f090bd..dc2f7a0e4 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -858,6 +858,14 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R state.Config.Values[k] = v } return ReduceOutcomeApplied + case *ahptypes.ChatMovedAction: + state.Provider = a.Provider + if state.Config != nil { + state.Config.Schema = a.Schema + } else if a.Schema != nil { + state.Config = &ahptypes.ChatConfigState{Schema: a.Schema, Values: map[string]json.RawMessage{}} + } + return ReduceOutcomeApplied } return ReduceOutcomeOutOfScope } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index e59c632af..49182b749 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -71,6 +71,7 @@ const ( ActionTypeChatIsReadChanged ActionType = "chat/isReadChanged" ActionTypeChatIsArchivedChanged ActionType = "chat/isArchivedChanged" ActionTypeChatConfigChanged ActionType = "chat/configChanged" + ActionTypeChatMoved ActionType = "chat/moved" ActionTypeChatInputRequested ActionType = "chat/inputRequested" ActionTypeChatInputAnswerChanged ActionType = "chat/inputAnswerChanged" ActionTypeChatInputCompleted ActionType = "chat/inputCompleted" @@ -889,6 +890,25 @@ type ChatConfigChangedAction struct { Replace *bool `json:"replace,omitempty"` } +// The chat moved to another session. +// +// Dispatched by the host on the moved chat's channel after a cross-session +// `moveChat`. `provider` and `schema` are the chat's +// {@link ChatState.provider | `provider`} and +// {@link ChatConfigState.schema | `config.schema`} relative to the new +// session; absent clears them. `ChatState` has no session field, so the +// reducer does not store `session`; it tells subscribers which session now +// owns the chat. +type ChatMovedAction struct { + Type ActionType `json:"type"` + // The new owning session URI. + Session URI `json:"session"` + // The chat's provider in the new session; absent clears it. + Provider *string `json:"provider,omitempty"` + // The chat's config schema in the new session; absent clears it. + Schema *SessionConfigSchema `json:"schema,omitempty"` +} + // A session requested input from the user. // // Creates an unresolved {@link InputRequestResponsePart} in the active turn, @@ -1872,6 +1892,7 @@ func (*ChatDraftChangedAction) isStateAction() {} func (*ChatIsReadChangedAction) isStateAction() {} func (*ChatIsArchivedChangedAction) isStateAction() {} func (*ChatConfigChangedAction) isStateAction() {} +func (*ChatMovedAction) isStateAction() {} func (*ChatInputRequestedAction) isStateAction() {} func (*ChatInputAnswerChangedAction) isStateAction() {} func (*ChatInputCompletedAction) isStateAction() {} @@ -2209,6 +2230,12 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "chat/moved": + var value ChatMovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value case "chat/inputRequested": var value ChatInputRequestedAction 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 5d66136a1..f7bb6290a 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -739,8 +739,9 @@ type ChatMoveToNewSessionDestination struct { // // A moved chat's `config.values` move with it unchanged. If a move changes // whether the chat needs its own `config.schema`, the host sets or clears it, -// or refuses the move as an unsupported destination. Clients SHOULD -// resubscribe to a moved chat whose `config.schema` changed. +// or refuses the move as an unsupported destination. After a cross-session +// move the host dispatches `chat/moved` on the chat's channel with its new +// session, `provider` and `config.schema`. // // On success every moved chat keeps its URI, state, and immutable // `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index ae744d8f7..24eccd2d3 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1398,9 +1398,10 @@ type ChatState struct { Interactivity *ChatInteractivity `json:"interactivity,omitempty"` // Agent provider ID backing this chat. Absent means the session's // {@link SessionMetadata.provider | `provider`}; hosts set it when it - // differs. It never changes: like `resource`, it is an identity field and is - // omitted from `session/chatUpdated` changes. Forks and side chats use the - // source chat's agent. + // differs. The chat's agent never changes, but `provider` is reported + // relative to its session, so `chat/moved` may set or clear it. Like + // `resource`, it is omitted from `session/chatUpdated` changes. Forks and + // side chats use the source chat's agent. // // Stability: 1.0 - Early development. Provider *string `json:"provider,omitempty"` 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 04f6335b1..abc6ecef7 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -1637,6 +1637,19 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when state.copy(config = current?.copy(values = newValues) ?: ChatConfigState(values = newValues)) } + is StateActionChatMoved -> { + val a = action.value + val current = state.config + state.copy( + provider = a.provider, + config = when { + current != null -> current.copy(schema = a.schema) + a.schema != null -> ChatConfigState(schema = a.schema, values = emptyMap()) + else -> null + }, + ) + } + else -> state } 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 5cf311528..10a509cc7 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 @@ -81,6 +81,7 @@ value class ActionType(val rawValue: String) { val CHAT_IS_READ_CHANGED: ActionType = ActionType("chat/isReadChanged") val CHAT_IS_ARCHIVED_CHANGED: ActionType = ActionType("chat/isArchivedChanged") val CHAT_CONFIG_CHANGED: ActionType = ActionType("chat/configChanged") + val CHAT_MOVED: ActionType = ActionType("chat/moved") val CHAT_INPUT_REQUESTED: ActionType = ActionType("chat/inputRequested") val CHAT_INPUT_ANSWER_CHANGED: ActionType = ActionType("chat/inputAnswerChanged") val CHAT_INPUT_COMPLETED: ActionType = ActionType("chat/inputCompleted") @@ -1071,6 +1072,23 @@ data class ChatConfigChangedAction( val replace: Boolean? = null ) +@Serializable +data class ChatMovedAction( + val type: ActionType, + /** + * The new owning session URI. + */ + val session: String, + /** + * The chat's provider in the new session; absent clears it. + */ + val provider: String? = null, + /** + * The chat's config schema in the new session; absent clears it. + */ + val schema: SessionConfigSchema? = null +) + @Serializable data class ChatInputRequestedAction( val type: ActionType, @@ -1769,6 +1787,7 @@ sealed interface StateAction @JvmInline value class StateActionChatIsReadChanged(val value: ChatIsReadChangedAction) : StateAction @JvmInline value class StateActionChatIsArchivedChanged(val value: ChatIsArchivedChangedAction) : StateAction @JvmInline value class StateActionChatConfigChanged(val value: ChatConfigChangedAction) : StateAction +@JvmInline value class StateActionChatMoved(val value: ChatMovedAction) : StateAction @JvmInline value class StateActionChatInputRequested(val value: ChatInputRequestedAction) : StateAction @JvmInline value class StateActionChatInputAnswerChanged(val value: ChatInputAnswerChangedAction) : StateAction @JvmInline value class StateActionChatInputCompleted(val value: ChatInputCompletedAction) : StateAction @@ -1891,6 +1910,7 @@ internal object StateActionSerializer : KSerializer { "chat/isReadChanged" -> StateActionChatIsReadChanged(input.json.decodeFromJsonElement(ChatIsReadChangedAction.serializer(), element)) "chat/isArchivedChanged" -> StateActionChatIsArchivedChanged(input.json.decodeFromJsonElement(ChatIsArchivedChangedAction.serializer(), element)) "chat/configChanged" -> StateActionChatConfigChanged(input.json.decodeFromJsonElement(ChatConfigChangedAction.serializer(), element)) + "chat/moved" -> StateActionChatMoved(input.json.decodeFromJsonElement(ChatMovedAction.serializer(), element)) "chat/inputRequested" -> StateActionChatInputRequested(input.json.decodeFromJsonElement(ChatInputRequestedAction.serializer(), element)) "chat/inputAnswerChanged" -> StateActionChatInputAnswerChanged(input.json.decodeFromJsonElement(ChatInputAnswerChangedAction.serializer(), element)) "chat/inputCompleted" -> StateActionChatInputCompleted(input.json.decodeFromJsonElement(ChatInputCompletedAction.serializer(), element)) @@ -2006,6 +2026,7 @@ internal object StateActionSerializer : KSerializer { is StateActionChatIsReadChanged -> output.json.encodeToJsonElement(ChatIsReadChangedAction.serializer(), value.value) is StateActionChatIsArchivedChanged -> output.json.encodeToJsonElement(ChatIsArchivedChangedAction.serializer(), value.value) is StateActionChatConfigChanged -> output.json.encodeToJsonElement(ChatConfigChangedAction.serializer(), value.value) + is StateActionChatMoved -> output.json.encodeToJsonElement(ChatMovedAction.serializer(), value.value) is StateActionChatInputRequested -> output.json.encodeToJsonElement(ChatInputRequestedAction.serializer(), value.value) is StateActionChatInputAnswerChanged -> output.json.encodeToJsonElement(ChatInputAnswerChangedAction.serializer(), value.value) is StateActionChatInputCompleted -> output.json.encodeToJsonElement(ChatInputCompletedAction.serializer(), value.value) 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 80bbdf6d6..f3c20688a 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 @@ -1814,9 +1814,10 @@ data class ChatState( /** * Agent provider ID backing this chat. Absent means the session's * {@link SessionMetadata.provider | `provider`}; hosts set it when it - * differs. It never changes: like `resource`, it is an identity field and is - * omitted from `session/chatUpdated` changes. Forks and side chats use the - * source chat's agent. + * differs. The chat's agent never changes, but `provider` is reported + * relative to its session, so `chat/moved` may set or clear it. Like + * `resource`, it is omitted from `session/chatUpdated` changes. Forks and + * side chats use the source chat's agent. * * Stability: 1.0 - Early development. */ diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 8d25d5dbf..6b005fcf2 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -20,10 +20,10 @@ use crate::state::{ ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, Message, - ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, - SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, - ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, - ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo, + ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionConfigSchema, + SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, + ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, + ToolCallRiskAssessment, ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -83,6 +83,7 @@ pub enum ActionType { ChatIsReadChanged, ChatIsArchivedChanged, ChatConfigChanged, + ChatMoved, ChatInputRequested, ChatInputAnswerChanged, ChatInputCompleted, @@ -228,6 +229,7 @@ impl serde::Serialize for ActionType { Self::ChatIsReadChanged => serializer.serialize_str("chat/isReadChanged"), Self::ChatIsArchivedChanged => serializer.serialize_str("chat/isArchivedChanged"), Self::ChatConfigChanged => serializer.serialize_str("chat/configChanged"), + Self::ChatMoved => serializer.serialize_str("chat/moved"), Self::ChatInputRequested => serializer.serialize_str("chat/inputRequested"), Self::ChatInputAnswerChanged => serializer.serialize_str("chat/inputAnswerChanged"), Self::ChatInputCompleted => serializer.serialize_str("chat/inputCompleted"), @@ -383,6 +385,7 @@ impl<'de> serde::Deserialize<'de> for ActionType { "chat/isReadChanged" => Self::ChatIsReadChanged, "chat/isArchivedChanged" => Self::ChatIsArchivedChanged, "chat/configChanged" => Self::ChatConfigChanged, + "chat/moved" => Self::ChatMoved, "chat/inputRequested" => Self::ChatInputRequested, "chat/inputAnswerChanged" => Self::ChatInputAnswerChanged, "chat/inputCompleted" => Self::ChatInputCompleted, @@ -1568,6 +1571,28 @@ pub struct ChatConfigChangedAction { pub replace: Option, } +/// The chat moved to another session. +/// +/// Dispatched by the host on the moved chat's channel after a cross-session +/// `moveChat`. `provider` and `schema` are the chat's +/// {@link ChatState.provider | `provider`} and +/// {@link ChatConfigState.schema | `config.schema`} relative to the new +/// session; absent clears them. `ChatState` has no session field, so the +/// reducer does not store `session`; it tells subscribers which session now +/// owns the chat. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatMovedAction { + /// The new owning session URI. + pub session: Uri, + /// The chat's provider in the new session; absent clears it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + /// The chat's config schema in the new session; absent clears it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schema: Option, +} + /// A session requested input from the user. /// /// Creates an unresolved {@link InputRequestResponsePart} in the active turn, @@ -2527,6 +2552,8 @@ pub enum StateAction { ChatIsArchivedChanged(ChatIsArchivedChangedAction), #[serde(rename = "chat/configChanged")] ChatConfigChanged(ChatConfigChangedAction), + #[serde(rename = "chat/moved")] + ChatMoved(ChatMovedAction), #[serde(rename = "chat/inputRequested")] ChatInputRequested(ChatInputRequestedAction), #[serde(rename = "chat/inputAnswerChanged")] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index ca227dc3b..19d2251cd 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -1025,8 +1025,9 @@ pub struct ChatMoveToNewSessionDestination {} /// /// A moved chat's `config.values` move with it unchanged. If a move changes /// whether the chat needs its own `config.schema`, the host sets or clears it, -/// or refuses the move as an unsupported destination. Clients SHOULD -/// resubscribe to a moved chat whose `config.schema` changed. +/// or refuses the move as an unsupported destination. After a cross-session +/// move the host dispatches `chat/moved` on the chat's channel with its new +/// session, `provider` and `config.schema`. /// /// On success every moved chat keeps its URI, state, and immutable /// `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index f5bf755d6..5adb6889e 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2065,9 +2065,10 @@ pub struct ChatState { pub interactivity: Option, /// Agent provider ID backing this chat. Absent means the session's /// {@link SessionMetadata.provider | `provider`}; hosts set it when it - /// differs. It never changes: like `resource`, it is an identity field and is - /// omitted from `session/chatUpdated` changes. Forks and side chats use the - /// source chat's agent. + /// differs. The chat's agent never changes, but `provider` is reported + /// relative to its session, so `chat/moved` may set or clear it. Like + /// `resource`, it is omitted from `session/chatUpdated` changes. Forks and + /// side chats use the source chat's agent. /// /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 79cae8a8f..26b355561 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -1433,6 +1433,20 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu } ReduceOutcome::Applied } + StateAction::ChatMoved(a) => { + state.provider = a.provider.clone(); + match state.config.as_mut() { + Some(config) => config.schema = a.schema.clone(), + None if a.schema.is_some() => { + state.config = Some(ChatConfigState { + schema: a.schema.clone(), + values: Default::default(), + }); + } + None => {} + } + ReduceOutcome::Applied + } _ => ReduceOutcome::OutOfScope, } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index de93675f5..513db0927 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -58,6 +58,7 @@ public enum ActionType: Codable, Sendable, Equatable { case chatIsReadChanged case chatIsArchivedChanged case chatConfigChanged + case chatMoved case chatInputRequested case chatInputAnswerChanged case chatInputCompleted @@ -172,6 +173,7 @@ public enum ActionType: Codable, Sendable, Equatable { case "chat/isReadChanged": self = .chatIsReadChanged case "chat/isArchivedChanged": self = .chatIsArchivedChanged case "chat/configChanged": self = .chatConfigChanged + case "chat/moved": self = .chatMoved case "chat/inputRequested": self = .chatInputRequested case "chat/inputAnswerChanged": self = .chatInputAnswerChanged case "chat/inputCompleted": self = .chatInputCompleted @@ -286,6 +288,7 @@ public enum ActionType: Codable, Sendable, Equatable { case .chatIsReadChanged: try container.encode("chat/isReadChanged") case .chatIsArchivedChanged: try container.encode("chat/isArchivedChanged") case .chatConfigChanged: try container.encode("chat/configChanged") + case .chatMoved: try container.encode("chat/moved") case .chatInputRequested: try container.encode("chat/inputRequested") case .chatInputAnswerChanged: try container.encode("chat/inputAnswerChanged") case .chatInputCompleted: try container.encode("chat/inputCompleted") @@ -1711,6 +1714,28 @@ public struct ChatConfigChangedAction: Codable, Sendable { } } +public struct ChatMovedAction: Codable, Sendable { + public var type: ActionType + /// The new owning session URI. + public var session: String + /// The chat's provider in the new session; absent clears it. + public var provider: String? + /// The chat's config schema in the new session; absent clears it. + public var schema: SessionConfigSchema? + + public init( + type: ActionType, + session: String, + provider: String? = nil, + schema: SessionConfigSchema? = nil + ) { + self.type = type + self.session = session + self.provider = provider + self.schema = schema + } +} + public struct ChatInputRequestedAction: Codable, Sendable { public var type: ActionType /// Input request to create or replace @@ -2661,6 +2686,7 @@ public enum StateAction: Codable, Sendable { case chatIsReadChanged(ChatIsReadChangedAction) case chatIsArchivedChanged(ChatIsArchivedChangedAction) case chatConfigChanged(ChatConfigChangedAction) + case chatMoved(ChatMovedAction) case chatInputRequested(ChatInputRequestedAction) case chatInputAnswerChanged(ChatInputAnswerChangedAction) case chatInputCompleted(ChatInputCompletedAction) @@ -2836,6 +2862,8 @@ public enum StateAction: Codable, Sendable { self = .chatIsArchivedChanged(try ChatIsArchivedChangedAction(from: decoder)) case "chat/configChanged": self = .chatConfigChanged(try ChatConfigChangedAction(from: decoder)) + case "chat/moved": + self = .chatMoved(try ChatMovedAction(from: decoder)) case "chat/inputRequested": self = .chatInputRequested(try ChatInputRequestedAction(from: decoder)) case "chat/inputAnswerChanged": @@ -3001,6 +3029,7 @@ public enum StateAction: Codable, Sendable { case .chatIsReadChanged(let v): try v.encode(to: encoder) case .chatIsArchivedChanged(let v): try v.encode(to: encoder) case .chatConfigChanged(let v): try v.encode(to: encoder) + case .chatMoved(let v): try v.encode(to: encoder) case .chatInputRequested(let v): try v.encode(to: encoder) case .chatInputAnswerChanged(let v): try v.encode(to: encoder) case .chatInputCompleted(let v): try v.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 5fd1ae686..b917181a3 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1824,9 +1824,10 @@ public struct ChatState: Codable, Sendable { public var interactivity: ChatInteractivity? /// Agent provider ID backing this chat. Absent means the session's /// {@link SessionMetadata.provider | `provider`}; hosts set it when it - /// differs. It never changes: like `resource`, it is an identity field and is - /// omitted from `session/chatUpdated` changes. Forks and side chats use the - /// source chat's agent. + /// differs. The chat's agent never changes, but `provider` is reported + /// relative to its session, so `chat/moved` may set or clear it. Like + /// `resource`, it is omitted from `session/chatUpdated` changes. Forks and + /// side chats use the source chat's agent. /// /// Stability: 1.0 - Early development. public var provider: String? diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 19f83134d..efb43f13a 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -734,6 +734,17 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { next.config = config return next + case .chatMoved(let a): + var next = state + next.provider = a.provider + if var config = next.config { + config.schema = a.schema + next.config = config + } else if let schema = a.schema { + next.config = ChatConfigState(schema: schema, values: [:]) + } + return next + default: return state } diff --git a/docs/.changes/20261026-chat-moved.json b/docs/.changes/20261026-chat-moved.json new file mode 100644 index 000000000..853c3ce96 --- /dev/null +++ b/docs/.changes/20261026-chat-moved.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Adds `chat/moved`, dispatched on a chat's channel after a cross-session move with its new session, `provider` and `config.schema`." +} diff --git a/docs/guide/actions.md b/docs/guide/actions.md index 9e4a5ef24..7bd2daa84 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -109,6 +109,7 @@ Tool calls follow a discriminated-union state machine — see [State Model — T | `session/isArchivedChanged` | **Yes** | Client archived or unarchived session | | `session/configChanged` | **Yes** | Mutable session config values changed | | `chat/configChanged` | **Yes** | Config values set on a chat (`chatMutable` properties) changed | +| `chat/moved` | No | The chat moved to another session; carries its new session, `provider` and `config.schema` | | `session/metaChanged` | No | The session's `_meta` side-channel was replaced | ### Server & Active-Client Tools (session channel) diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 88b00a98d..4d63a2a4c 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -101,7 +101,8 @@ reject keys that are not `chatMutable` in the effective schema. Every chat is backed by one agent. `SessionState.provider` is the session's agent, and [`ChatSummary.provider`](/reference/chat#chatsummary) names a chat's agent when it differs; absence means the session's agent. A chat's agent never -changes. +changes, but `provider` is relative to its session, so `chat/moved` may set or +clear it. Hosts that advertise [`multipleChatProviders`](/reference/common#initializeresult) accept a @@ -259,8 +260,9 @@ an unsupported destination, a move they cannot support. A moved chat's `config.values` move with it unchanged. If a move changes whether the chat needs its own `config.schema`, the host sets or clears it, or refuses -the move as an unsupported destination. Clients SHOULD resubscribe to a moved -chat whose `config.schema` changed. +the move as an unsupported destination. After a cross-session move the host +dispatches [`chat/moved`](/reference/chat#chatmovedaction) on the chat's channel +with its new session, `provider` and `config.schema`. #### Atomic synchronization diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 6e064410e..37553a92e 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -1734,6 +1734,31 @@ "config" ] }, + "ChatMovedAction": { + "type": "object", + "description": "The chat moved to another session.\n\nDispatched by the host on the moved chat's channel after a cross-session\n`moveChat`. `provider` and `schema` are the chat's\n{@link ChatState.provider | `provider`} and\n{@link ChatConfigState.schema | `config.schema`} relative to the new\nsession; absent clears them. `ChatState` has no session field, so the\nreducer does not store `session`; it tells subscribers which session now\nowns the chat.", + "properties": { + "type": { + "const": "chat/moved" + }, + "session": { + "$ref": "#/$defs/URI", + "description": "The new owning session URI." + }, + "provider": { + "type": "string", + "description": "The chat's provider in the new session; absent clears it." + }, + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "The chat's config schema in the new session; absent clears it." + } + }, + "required": [ + "type", + "session" + ] + }, "ChatInputRequestedAction": { "type": "object", "description": "A session requested input from the user.\n\nCreates an unresolved {@link InputRequestResponsePart} in the active turn,\nor replaces the unresolved part with the same request `id`. Answer drafts\nare preserved unless `request.answers` is provided.", @@ -2673,6 +2698,9 @@ { "$ref": "#/$defs/ChatConfigChangedAction" }, + { + "$ref": "#/$defs/ChatMovedAction" + }, { "$ref": "#/$defs/ChatInputRequestedAction" }, @@ -5230,7 +5258,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. The chat's agent never changes, but `provider` is reported\nrelative to its session, so `chat/moved` may set or clear it. Like\n`resource`, it is omitted from `session/chatUpdated` changes. Forks and\nside chats use the source chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -9482,6 +9510,9 @@ { "$ref": "#/$defs/ChatConfigChangedAction" }, + { + "$ref": "#/$defs/ChatMovedAction" + }, { "$ref": "#/$defs/ChatInputRequestedAction" }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index e3ccdcd9e..b8ab3a3a2 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1509,7 +1509,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host sets or clears it,\nor refuses the move as an unsupported destination. Clients SHOULD\nresubscribe to a moved chat whose `config.schema` changed.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host sets or clears it,\nor refuses the move as an unsupported destination. After a cross-session\nmove the host dispatches `chat/moved` on the chat's channel with its new\nsession, `provider` and `config.schema`.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -4551,7 +4551,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. The chat's agent never changes, but `provider` is reported\nrelative to its session, so `chat/moved` may set or clear it. Like\n`resource`, it is omitted from `session/chatUpdated` changes. Forks and\nside chats use the source chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -9755,6 +9755,31 @@ "config" ] }, + "ChatMovedAction": { + "type": "object", + "description": "The chat moved to another session.\n\nDispatched by the host on the moved chat's channel after a cross-session\n`moveChat`. `provider` and `schema` are the chat's\n{@link ChatState.provider | `provider`} and\n{@link ChatConfigState.schema | `config.schema`} relative to the new\nsession; absent clears them. `ChatState` has no session field, so the\nreducer does not store `session`; it tells subscribers which session now\nowns the chat.", + "properties": { + "type": { + "const": "chat/moved" + }, + "session": { + "$ref": "#/$defs/URI", + "description": "The new owning session URI." + }, + "provider": { + "type": "string", + "description": "The chat's provider in the new session; absent clears it." + }, + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "The chat's config schema in the new session; absent clears it." + } + }, + "required": [ + "type", + "session" + ] + }, "ChatInputRequestedAction": { "type": "object", "description": "A session requested input from the user.\n\nCreates an unresolved {@link InputRequestResponsePart} in the active turn,\nor replaces the unresolved part with the same request `id`. Answer drafts\nare preserved unless `request.answers` is provided.", @@ -10786,6 +10811,9 @@ { "$ref": "#/$defs/ChatConfigChangedAction" }, + { + "$ref": "#/$defs/ChatMovedAction" + }, { "$ref": "#/$defs/ChatInputRequestedAction" }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 2d9d56298..58d9577ad 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2640,7 +2640,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. The chat's agent never changes, but `provider` is reported\nrelative to its session, so `chat/moved` may set or clear it. Like\n`resource`, it is omitted from `session/chatUpdated` changes. Forks and\nside chats use the source chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", @@ -7619,7 +7619,7 @@ }, "MoveChatParams": { "type": "object", - "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host sets or clears it,\nor refuses the move as an unsupported destination. Clients SHOULD\nresubscribe to a moved chat whose `config.schema` changed.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", + "description": "Atomically moves a host-authorized chat within or between sessions.\n\nThe source is the chat named by `channel`. When a `session` destination is\nthe source's current session, only the requested entry is repositioned in\nthat session's public chat catalog. When it names another session, the host\ntransfers the requested chat and its complete host-managed descendant\nhierarchy. The optional `after` anchor positions the requested chat in the\ndestination catalog; when omitted, the requested chat is placed first.\n\nA `newSession` destination allocates a session, transfers the complete\nhierarchy, and makes the requested chat that session's non-movable default\nchat. The host owns descendant relationships; AHP does not expose them as\nchat state.\n\nClients MUST only request a move when the source chat advertises\n`movable: true` in its `ChatState` or `ChatSummary`. This is structural\neligibility, not a guarantee that request-specific validation will succeed.\n\nThe host MUST validate the complete operation before committing it and MAY\nreject unsupported destinations or transient source conditions. At minimum,\nthe source MUST exist and advertise `movable: true`; the destination and\noptional anchor MUST resolve; and the source MUST NOT anchor itself.\nRejection leaves ownership, catalog order, chat state, and root summaries\nunchanged.\n\nA moved chat keeps its agent. After the move the host reports\n{@link ChatState.provider | `provider`} on the chat whenever it differs from\nthe destination session's provider, including when the chat had none\nbecause it matched its old session. Hosts MAY refuse, as an unsupported\ndestination, a move they cannot support, such as one that changes whether\nthe chat's agent matches its session's.\n\nA moved chat's `config.values` move with it unchanged. If a move changes\nwhether the chat needs its own `config.schema`, the host sets or clears it,\nor refuses the move as an unsupported destination. After a cross-session\nmove the host dispatches `chat/moved` on the chat's channel with its new\nsession, `provider` and `config.schema`.\n\nOn success every moved chat keeps its URI, state, and immutable\n`ChatOrigin`. The host commits ownership and catalog order before publishing\n`session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and\nroot summary updates as applicable. Session and root snapshots are the\ndurable recovery path after reconnect or an uncertain response.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -9005,6 +9005,9 @@ { "$ref": "#/$defs/ChatConfigChangedAction" }, + { + "$ref": "#/$defs/ChatMovedAction" + }, { "$ref": "#/$defs/ChatInputRequestedAction" }, @@ -10811,6 +10814,31 @@ "config" ] }, + "ChatMovedAction": { + "type": "object", + "description": "The chat moved to another session.\n\nDispatched by the host on the moved chat's channel after a cross-session\n`moveChat`. `provider` and `schema` are the chat's\n{@link ChatState.provider | `provider`} and\n{@link ChatConfigState.schema | `config.schema`} relative to the new\nsession; absent clears them. `ChatState` has no session field, so the\nreducer does not store `session`; it tells subscribers which session now\nowns the chat.", + "properties": { + "type": { + "const": "chat/moved" + }, + "session": { + "$ref": "#/$defs/URI", + "description": "The new owning session URI." + }, + "provider": { + "type": "string", + "description": "The chat's provider in the new session; absent clears it." + }, + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "The chat's config schema in the new session; absent clears it." + } + }, + "required": [ + "type", + "session" + ] + }, "ChatInputRequestedAction": { "type": "object", "description": "A session requested input from the user.\n\nCreates an unresolved {@link InputRequestResponsePart} in the active turn,\nor replaces the unresolved part with the same request `id`. Answer drafts\nare preserved unless `request.answers` is provided.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 27e572b97..ad27dabb4 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2942,7 +2942,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. The chat's agent never changes, but `provider` is reported\nrelative to its session, so `chat/moved` may set or clear it. Like\n`resource`, it is omitted from `session/chatUpdated` changes. Forks and\nside chats use the source chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", diff --git a/schema/state.schema.json b/schema/state.schema.json index c0b011a5e..4d750087e 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2551,7 +2551,7 @@ }, "provider": { "type": "string", - "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. It never changes: like `resource`, it is an identity field and is\nomitted from `session/chatUpdated` changes. Forks and side chats use the\nsource chat's agent.\n\nStability: 1.0 - Early development." + "description": "Agent provider ID backing this chat. Absent means the session's\n{@link SessionMetadata.provider | `provider`}; hosts set it when it\ndiffers. The chat's agent never changes, but `provider` is reported\nrelative to its session, so `chat/moved` may set or clear it. Like\n`resource`, it is omitted from `session/chatUpdated` changes. Forks and\nside chats use the source chat's agent.\n\nStability: 1.0 - Early development." }, "workingDirectories": { "type": "array", diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index eb4903da4..0f71d3ec0 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -1714,6 +1714,7 @@ const ACTION_VARIANTS: { type: string; variantName: string; tsInterface: string { type: 'chat/isReadChanged', variantName: 'ChatIsReadChanged', tsInterface: 'ChatIsReadChangedAction' }, { type: 'chat/isArchivedChanged', variantName: 'ChatIsArchivedChanged', tsInterface: 'ChatIsArchivedChangedAction' }, { type: 'chat/configChanged', variantName: 'ChatConfigChanged', tsInterface: 'ChatConfigChangedAction' }, + { type: 'chat/moved', variantName: 'ChatMoved', tsInterface: 'ChatMovedAction' }, { type: 'chat/inputRequested', variantName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', variantName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', variantName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 289be7469..e0dfc2e17 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1629,6 +1629,7 @@ const ACTION_VARIANTS: { { type: 'chat/isReadChanged', variantName: 'ChatIsReadChanged', tsInterface: 'ChatIsReadChangedAction' }, { type: 'chat/isArchivedChanged', variantName: 'ChatIsArchivedChanged', tsInterface: 'ChatIsArchivedChangedAction' }, { type: 'chat/configChanged', variantName: 'ChatConfigChanged', tsInterface: 'ChatConfigChangedAction' }, + { type: 'chat/moved', variantName: 'ChatMoved', tsInterface: 'ChatMovedAction' }, { type: 'chat/inputRequested', variantName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', variantName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', variantName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 1899e655f..8cbffe506 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1571,6 +1571,7 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'chat/isReadChanged', caseName: 'ChatIsReadChanged', tsInterface: 'ChatIsReadChangedAction' }, { type: 'chat/isArchivedChanged', caseName: 'ChatIsArchivedChanged', tsInterface: 'ChatIsArchivedChangedAction' }, { type: 'chat/configChanged', caseName: 'ChatConfigChanged', tsInterface: 'ChatConfigChangedAction' }, + { type: 'chat/moved', caseName: 'ChatMoved', tsInterface: 'ChatMovedAction' }, { type: 'chat/inputRequested', caseName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', caseName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', caseName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 8db883f5e..cdc31ad27 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1590,6 +1590,7 @@ const ACTION_VARIANTS: { { type: 'chat/isReadChanged', variantName: 'ChatIsReadChanged', tsInterface: 'ChatIsReadChangedAction' }, { type: 'chat/isArchivedChanged', variantName: 'ChatIsArchivedChanged', tsInterface: 'ChatIsArchivedChangedAction' }, { type: 'chat/configChanged', variantName: 'ChatConfigChanged', tsInterface: 'ChatConfigChangedAction' }, + { type: 'chat/moved', variantName: 'ChatMoved', tsInterface: 'ChatMovedAction' }, { type: 'chat/inputRequested', variantName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', variantName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', variantName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, @@ -1717,7 +1718,7 @@ impl Serialize for ChatErrorAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, BackgroundWork, CanvasReference, CanvasState, ChangesSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, BackgroundWork, CanvasReference, CanvasState, ChangesSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionConfigSchema, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); lines.push(''); // ActionType enum lines.push('// ─── ActionType ──────────────────────────────────────────────────────\n'); diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index a1fe2af36..69ed7ac63 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1484,6 +1484,7 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'chat/isReadChanged', caseName: 'chatIsReadChanged', tsInterface: 'ChatIsReadChangedAction' }, { type: 'chat/isArchivedChanged', caseName: 'chatIsArchivedChanged', tsInterface: 'ChatIsArchivedChangedAction' }, { type: 'chat/configChanged', caseName: 'chatConfigChanged', tsInterface: 'ChatConfigChangedAction' }, + { type: 'chat/moved', caseName: 'chatMoved', tsInterface: 'ChatMovedAction' }, { type: 'chat/inputRequested', caseName: 'chatInputRequested', tsInterface: 'ChatInputRequestedAction' }, { type: 'chat/inputAnswerChanged', caseName: 'chatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, { type: 'chat/inputCompleted', caseName: 'chatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 89b6c876d..f3f6281d7 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -71,6 +71,7 @@ import type { ChatIsReadChangedAction, ChatIsArchivedChangedAction, ChatConfigChangedAction, + ChatMovedAction, ChatInputRequestedAction, ChatInputAnswerChangedAction, ChatInputCompletedAction, @@ -243,6 +244,7 @@ export type ChatAction = | ChatIsReadChangedAction | ChatIsArchivedChangedAction | ChatConfigChangedAction + | ChatMovedAction | ChatInputRequestedAction | ChatInputAnswerChangedAction | ChatInputCompletedAction @@ -292,6 +294,7 @@ export type ServerChatAction = | ChatCanvasesChangedAction | ChatUsageAction | ChatReasoningAction + | ChatMovedAction | ChatInputRequestedAction | ChatTurnsLoadedAction ; @@ -528,6 +531,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.ChatIsReadChanged]: true, [ActionType.ChatIsArchivedChanged]: true, [ActionType.ChatConfigChanged]: true, + [ActionType.ChatMoved]: false, [ActionType.ChatInputRequested]: false, [ActionType.ChatInputAnswerChanged]: true, [ActionType.ChatInputCompleted]: true, diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index cd2754057..ca1fb4155 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -8,7 +8,7 @@ import { ActionType } from '../common/actions.js'; import type { StringOrMarkdown, FileEditCollection, UsageInfo, URI } from '../common/state.js'; import type { CanvasReference } from '../channels-canvas/state.js'; import type { Changeset } from '../channels-changeset/state.js'; -import type { McpAuthRequirement } from '../channels-session/state.js'; +import type { McpAuthRequirement, SessionConfigSchema } from '../channels-session/state.js'; import type { BackgroundWork, Message, @@ -922,6 +922,30 @@ export interface ChatConfigChangedAction { replace?: boolean; } +/** + * The chat moved to another session. + * + * Dispatched by the host on the moved chat's channel after a cross-session + * `moveChat`. `provider` and `schema` are the chat's + * {@link ChatState.provider | `provider`} and + * {@link ChatConfigState.schema | `config.schema`} relative to the new + * session; absent clears them. `ChatState` has no session field, so the + * reducer does not store `session`; it tells subscribers which session now + * owns the chat. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatMovedAction { + type: ActionType.ChatMoved; + /** The new owning session URI. */ + session: URI; + /** The chat's provider in the new session; absent clears it. */ + provider?: string; + /** The chat's config schema in the new session; absent clears it. */ + schema?: SessionConfigSchema; +} + // ─── Session Input Actions ────────────────────────────────────────────────── /** @@ -1017,6 +1041,7 @@ export type ChatAction = | ChatIsReadChangedAction | ChatIsArchivedChangedAction | ChatConfigChangedAction + | ChatMovedAction | ChatInputRequestedAction | ChatInputAnswerChangedAction | ChatInputCompletedAction diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index ac3daa946..dd2068ed9 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -210,8 +210,9 @@ export type ChatMoveDestination = * * A moved chat's `config.values` move with it unchanged. If a move changes * whether the chat needs its own `config.schema`, the host sets or clears it, - * or refuses the move as an unsupported destination. Clients SHOULD - * resubscribe to a moved chat whose `config.schema` changed. + * or refuses the move as an unsupported destination. After a cross-session + * move the host dispatches `chat/moved` on the chat's channel with its new + * session, `provider` and `config.schema`. * * On success every moved chat keeps its URI, state, and immutable * `ChatOrigin`. The host commits ownership and catalog order before publishing diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts index 5f26e0a4e..a4e9b9be9 100644 --- a/types/channels-chat/reducer.ts +++ b/types/channels-chat/reducer.ts @@ -979,6 +979,18 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st }, }; + case ActionType.ChatMoved: { + const { provider: _provider, config, ...rest } = state; + const next: ChatState = action.provider === undefined ? rest : { ...rest, provider: action.provider }; + if (config) { + const { schema: _schema, ...values } = config; + next.config = action.schema === undefined ? values : { ...values, schema: action.schema }; + } else if (action.schema !== undefined) { + next.config = { schema: action.schema, values: {} }; + } + return next; + } + default: softAssertNever(action, log); return state; diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 9fc00c6e0..ac66855da 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -86,9 +86,10 @@ export interface ChatState { /** * Agent provider ID backing this chat. Absent means the session's * {@link SessionMetadata.provider | `provider`}; hosts set it when it - * differs. It never changes: like `resource`, it is an identity field and is - * omitted from `session/chatUpdated` changes. Forks and side chats use the - * source chat's agent. + * differs. The chat's agent never changes, but `provider` is reported + * relative to its session, so `chat/moved` may set or clear it. Like + * `resource`, it is omitted from `session/chatUpdated` changes. Forks and + * side chats use the source chat's agent. * @stability 1.0 */ provider?: string; diff --git a/types/common/actions.ts b/types/common/actions.ts index ec23da4b5..55125bf9e 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -82,6 +82,7 @@ import type { ChatIsReadChangedAction, ChatIsArchivedChangedAction, ChatConfigChangedAction, + ChatMovedAction, ChatInputRequestedAction, ChatInputAnswerChangedAction, ChatInputCompletedAction, @@ -204,6 +205,7 @@ export const enum ActionType { ChatIsReadChanged = 'chat/isReadChanged', ChatIsArchivedChanged = 'chat/isArchivedChanged', ChatConfigChanged = 'chat/configChanged', + ChatMoved = 'chat/moved', ChatInputRequested = 'chat/inputRequested', ChatInputAnswerChanged = 'chat/inputAnswerChanged', ChatInputCompleted = 'chat/inputCompleted', @@ -363,6 +365,7 @@ export type StateAction = | ChatIsReadChangedAction | ChatIsArchivedChangedAction | ChatConfigChangedAction + | ChatMovedAction | ChatInputRequestedAction | ChatInputAnswerChangedAction | ChatInputCompletedAction diff --git a/types/test-cases/reducers/320-chat-moved-sets-provider-and-schema.json b/types/test-cases/reducers/320-chat-moved-sets-provider-and-schema.json new file mode 100644 index 000000000..034e19228 --- /dev/null +++ b/types/test-cases/reducers/320-chat-moved-sets-provider-and-schema.json @@ -0,0 +1,58 @@ +{ + "description": "chat/moved sets the chat's provider and config schema relative to the new session and preserves its config values", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "config": { + "values": { + "autoApprove": true + } + } + }, + "actions": [ + { + "type": "chat/moved", + "session": "ahp-session:/s2", + "provider": "other-agent", + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + } + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "provider": "other-agent", + "config": { + "values": { + "autoApprove": true + }, + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + } + } + } +} diff --git a/types/test-cases/reducers/321-chat-moved-clears-provider-and-schema.json b/types/test-cases/reducers/321-chat-moved-clears-provider-and-schema.json new file mode 100644 index 000000000..6b8d5f099 --- /dev/null +++ b/types/test-cases/reducers/321-chat-moved-clears-provider-and-schema.json @@ -0,0 +1,46 @@ +{ + "description": "chat/moved without provider or schema clears both and preserves the chat's config values", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": { + "autoApprove": true + } + } + }, + "actions": [ + { + "type": "chat/moved", + "session": "ahp-session:/s1" + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "config": { + "values": { + "autoApprove": true + } + } + } +} diff --git a/types/test-cases/reducers/322-chat-moved-creates-config-for-schema.json b/types/test-cases/reducers/322-chat-moved-creates-config-for-schema.json new file mode 100644 index 000000000..b5190e02c --- /dev/null +++ b/types/test-cases/reducers/322-chat-moved-creates-config-for-schema.json @@ -0,0 +1,51 @@ +{ + "description": "chat/moved with a schema creates the chat's config when it has none", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" + }, + "actions": [ + { + "type": "chat/moved", + "session": "ahp-session:/s2", + "provider": "other-agent", + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + } + } + ], + "expected": { + "turns": [], + "resource": "ahp-chat:/c2", + "title": "Chat", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "provider": "other-agent", + "config": { + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + }, + "values": {} + } + } +} diff --git a/types/test-cases/round-trips/084-chat-moved-action.json b/types/test-cases/round-trips/084-chat-moved-action.json new file mode 100644 index 000000000..a4b764aa6 --- /dev/null +++ b/types/test-cases/round-trips/084-chat-moved-action.json @@ -0,0 +1,40 @@ +{ + "name": "chat-moved-action", + "group": "A", + "description": "A chat/moved action preserves the new session, provider and config schema.", + "type": "StateAction", + "input": { + "type": "chat/moved", + "session": "ahp-session:/s2", + "provider": "other-agent", + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + } + }, + "acceptableOutputs": [ + { + "type": "chat/moved", + "session": "ahp-session:/s2", + "provider": "other-agent", + "schema": { + "type": "object", + "properties": { + "autoApprove": { + "type": "boolean", + "title": "Auto-approve tools", + "default": false, + "chatMutable": true + } + } + } + } + ] +} diff --git a/types/version/registry.ts b/types/version/registry.ts index 3c0982afa..d25e68643 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -165,6 +165,7 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChatIsReadChanged]: '0.9.0', [ActionType.ChatIsArchivedChanged]: '0.9.0', [ActionType.ChatConfigChanged]: '1.1.0', + [ActionType.ChatMoved]: '1.1.0', [ActionType.ChatInputRequested]: '0.4.0', [ActionType.ChatInputAnswerChanged]: '0.4.0', [ActionType.ChatInputCompleted]: '0.4.0',