diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index 6f7f13f26..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"); @@ -1180,7 +1182,7 @@ public sealed record SessionChatUpdatedAction /// Mutable summary fields that changed; omitted fields are unchanged. /// - /// Identity fields (`resource`) never change and MUST be omitted by + /// Identity fields (`resource`, `provider`) never change and MUST be omitted by /// senders; receivers SHOULD ignore them if present. public required PartialChatSummary Changes { get; init; } } @@ -2092,10 +2094,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 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 { public ActionType Type { get; init; } = ActionType.ChatConfigChanged; @@ -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, @@ -2782,6 +2809,12 @@ public sealed record PartialChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; init; } + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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)] @@ -2897,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 65d398ecf..6151529b0 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -485,6 +485,14 @@ public sealed record InitializeResult /// Stability: 1.0 - Early development. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public TcpConnectionsCapability? TcpConnections { get; init; } + + /// 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. + /// + /// 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 @@ -614,6 +622,13 @@ public sealed record AutomationCustomizationsCapability { } +/// Presence capability for {@link InitializeResult.multipleChatProviders}. +/// +/// 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 @@ -1015,6 +1030,19 @@ 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 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` 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)] + public string? Provider { get; init; } } /// Disposes a chat and cleans up server-side resources. @@ -1079,6 +1107,19 @@ public sealed record ChatMoveToNewSessionDestination /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// +/// 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. +/// +/// 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. 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 /// `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 7d3e2ec90..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))] @@ -286,6 +287,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/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 61894abdb..f1341c718 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2365,6 +2365,12 @@ public sealed class ChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; set; } + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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 +2498,17 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; set; } + /// Agent provider ID backing this chat. Absent means the session's + /// {@link SessionMetadata.provider | `provider`}; hosts set it when it + /// 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)] + 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 @@ -2536,9 +2553,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; } @@ -3343,6 +3358,12 @@ public sealed record SessionChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChatInteractivity? Interactivity { get; init; } + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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} @@ -3464,8 +3485,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; } } @@ -3497,16 +3518,30 @@ 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. `values` holds the chat's `chatMutable` properties +/// 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: a chat's `values` change only through +/// `chat/configChanged` on the chat. /// -/// 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. +/// 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 values set on this chat, keyed by property id + /// 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 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/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 d99ff275f..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" @@ -224,7 +225,7 @@ 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 + // Identity fields (`resource`, `provider`) never change and MUST be omitted by // senders; receivers SHOULD ignore them if present. Changes PartialChatSummary `json:"changes"` } @@ -877,10 +878,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 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"` // Config values to set on the chat @@ -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 25c928454..f7bb6290a 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -231,6 +231,12 @@ type InitializeResult struct { // // Stability: 1.0 - Early development. 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. + // + // Stability: 1.0 - Early development. + MultipleChatProviders *MultipleChatProvidersCapability `json:"multipleChatProviders,omitempty"` } // Optional capabilities a client declares during `initialize`. @@ -314,6 +320,12 @@ type AutomationRunCancellationCapability struct { type AutomationCustomizationsCapability struct { } +// Presence capability for {@link InitializeResult.multipleChatProviders}. +// +// 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 @@ -652,6 +664,17 @@ 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 session's provider. + // + // Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + // `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"` } // Disposes a chat and cleans up server-side resources. @@ -707,6 +730,19 @@ type ChatMoveToNewSessionDestination struct { // Rejection leaves ownership, catalog order, chat state, and root summaries // unchanged. // +// 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. +// +// 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. 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 // `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index c4e5c2e6c..24eccd2d3 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1320,6 +1320,10 @@ 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. See {@link ChatState.provider}. + // + // 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 +1396,15 @@ type ChatState struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. 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. 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"` // 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 @@ -1428,9 +1441,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"` @@ -1535,6 +1546,10 @@ type ChatSummary struct { // Absence defaults to {@link ChatInteractivity.Full} for backward // compatibility. Interactivity *ChatInteractivity `json:"interactivity,omitempty"` + // Agent provider ID backing this chat. See {@link ChatState.provider}. + // + // 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"` @@ -1666,8 +1681,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"` } @@ -1692,15 +1707,27 @@ 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. `values` holds the chat's `chatMutable` properties +// 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: a chat's `values` change only through +// `chat/configChanged` on the chat. // -// 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. +// 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 values set on this chat, keyed by property id + // 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 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/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index 6aaa44802..abc6ecef7 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,20 @@ 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)) + } + + 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 467a7f67b..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") @@ -231,7 +232,7 @@ data class SessionChatUpdatedAction( /** * Mutable summary fields that changed; omitted fields are unchanged. * - * Identity fields (`resource`) never change and MUST be omitted by + * Identity fields (`resource`, `provider`) never change and MUST be omitted by * senders; receivers SHOULD ignore them if present. */ val changes: PartialChatSummary @@ -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, @@ -1686,6 +1704,12 @@ data class PartialChatSummary( * compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID backing this chat. See {@link ChatState.provider}. + * + * 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. @@ -1763,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 @@ -1885,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)) @@ -2000,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/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 95dcccd9c..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 @@ -487,7 +487,15 @@ data class InitializeResult( * * Stability: 1.0 - Early development. */ - val tcpConnections: TcpConnectionsCapability? = null + val tcpConnections: TcpConnectionsCapability? = null, + /** + * 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. + * + * Stability: 1.0 - Early development. + */ + val multipleChatProviders: MultipleChatProvidersCapability? = null ) @Serializable @@ -555,6 +563,14 @@ class AutomationRunCancellationCapability @Serializable class AutomationCustomizationsCapability +/** + * Presence capability for {@link InitializeResult.multipleChatProviders}. + * + * Stability: 1.0 - Early development. + */ +@Serializable +class MultipleChatProvidersCapability + @Serializable data class Implementation( /** @@ -925,7 +941,20 @@ data class CreateChatParams( /** * Client-selected initial display title for the new chat. */ - val title: String? = null + val title: String? = null, + /** + * 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` 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. + */ + val provider: String? = null ) @Serializable @@ -1705,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 8261d8cdf..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 @@ -1811,6 +1811,17 @@ data class ChatState( * compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID backing this chat. Absent means the session's + * {@link SessionMetadata.provider | `provider`}; hosts set it when it + * 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. + */ + val provider: String? = null, /** * The subset of the session's * {@link SessionState.workingDirectories | `workingDirectories`} that this @@ -1856,9 +1867,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, /** @@ -1959,6 +1968,12 @@ data class ChatSummary( * compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID backing this chat. See {@link ChatState.provider}. + * + * 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. @@ -2484,6 +2499,12 @@ data class SessionChatSummary( * backward compatibility. */ val interactivity: ChatInteractivity? = null, + /** + * Agent provider ID backing this chat. See {@link ChatState.provider}. + * + * Stability: 1.0 - Early development. + */ + val provider: String? = null, /** * Current chat status, matching {@link ChatSummary.status}. * @@ -2548,7 +2569,18 @@ data class SessionConfigState( @Serializable data class ChatConfigState( /** - * Config values set on this chat, keyed by property id + * 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 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 cf1b6f37f..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, @@ -558,7 +561,7 @@ 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 + /// Identity fields (`resource`, `provider`) never change and MUST be omitted by /// senders; receivers SHOULD ignore them if present. pub changes: PartialChatSummary, } @@ -1554,10 +1557,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 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")] pub struct ChatConfigChangedAction { @@ -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, @@ -2396,6 +2421,11 @@ pub struct PartialChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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")] @@ -2522,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 df79aebaa..19d2251cd 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -402,6 +402,13 @@ pub struct InitializeResult { /// Stability: 1.0 - Early development. #[serde(default, skip_serializing_if = "Option::is_none")] 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. + /// + /// 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`. @@ -501,6 +508,13 @@ pub struct AutomationRunCancellationCapability {} #[serde(rename_all = "camelCase")] pub struct AutomationCustomizationsCapability {} +/// Presence capability for {@link InitializeResult.multipleChatProviders}. +/// +/// 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 @@ -932,6 +946,18 @@ 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 session's provider. + /// + /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + /// `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")] + pub provider: Option, } /// Disposes a chat and cleans up server-side resources. @@ -990,6 +1016,19 @@ pub struct ChatMoveToNewSessionDestination {} /// Rejection leaves ownership, catalog order, chat state, and root summaries /// unchanged. /// +/// 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. +/// +/// 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. 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 /// `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 7cf83e4ff..5adb6889e 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2063,6 +2063,16 @@ pub struct ChatState { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID backing this chat. Absent means the session's + /// {@link SessionMetadata.provider | `provider`}; hosts set it when it + /// 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")] + 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 @@ -2103,9 +2113,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 @@ -2232,6 +2240,11 @@ pub struct ChatSummary { /// compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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")] @@ -2736,6 +2749,11 @@ pub struct SessionChatSummary { /// backward compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] pub interactivity: Option, + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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} @@ -2840,8 +2858,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, } @@ -2872,17 +2890,30 @@ 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. `values` holds the chat's `chatMutable` properties +/// 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: a chat's `values` change only through +/// `chat/configChanged` on the chat. /// -/// 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. +/// 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 values set on this chat, keyed by property id + /// 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 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/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index bbf08d72f..26b355561 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) { @@ -1432,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, } } @@ -2324,6 +2339,7 @@ mod tests { origin: None, movable: None, interactivity: None, + provider: None, working_directories: None, changesets: None, canvases: None, @@ -2485,6 +2501,7 @@ mod tests { origin: None, movable: None, interactivity: None, + provider: None, working_directories: None, }; let added = StateAction::SessionChatAdded(ahp_types::actions::SessionChatAddedAction { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 14a77834d..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") @@ -472,7 +475,7 @@ 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 + /// Identity fields (`resource`, `provider`) never change and MUST be omitted by /// senders; receivers SHOULD ignore them if present. public var changes: PartialChatSummary @@ -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 @@ -2566,6 +2591,10 @@ public struct PartialChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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 +2609,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 +2621,7 @@ public struct PartialChatSummary: Codable, Sendable { self.origin = origin self.movable = movable self.interactivity = interactivity + self.provider = provider self.workingDirectories = workingDirectories } } @@ -2655,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) @@ -2830,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": @@ -2995,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/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index e8ebbec76..b30ec44df 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -447,6 +447,12 @@ public struct InitializeResult: Codable, Sendable { /// /// Stability: 1.0 - Early development. 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. + /// + /// Stability: 1.0 - Early development. + public var multipleChatProviders: MultipleChatProvidersCapability? enum CodingKeys: String, CodingKey { case protocolVersion @@ -460,6 +466,7 @@ public struct InitializeResult: Codable, Sendable { case telemetry case automations case tcpConnections + case multipleChatProviders } public init( @@ -473,7 +480,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 @@ -486,6 +494,7 @@ public struct InitializeResult: Codable, Sendable { self.telemetry = telemetry self.automations = automations self.tcpConnections = tcpConnections + self.multipleChatProviders = multipleChatProviders } } @@ -578,6 +587,17 @@ public struct AutomationCustomizationsCapability: Codable, Sendable { } } +/// Presence capability for {@link InitializeResult.multipleChatProviders}. +/// +/// 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 @@ -999,6 +1019,17 @@ 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 session's provider. + /// + /// Requires {@link InitializeResult.multipleChatProviders}. Hosts reject with + /// `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? enum CodingKeys: String, CodingKey { case channel @@ -1008,6 +1039,7 @@ public struct CreateChatParams: Codable, Sendable { case source case workingDirectories case title + case provider } public init( @@ -1017,7 +1049,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 @@ -1026,6 +1059,7 @@ public struct CreateChatParams: Codable, Sendable { self.source = source self.workingDirectories = workingDirectories self.title = title + self.provider = provider } } @@ -2025,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 bff92194f..b917181a3 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1822,6 +1822,15 @@ public struct ChatState: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. 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. 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? /// 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 @@ -1858,9 +1867,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] @@ -1907,6 +1914,7 @@ public struct ChatState: Codable, Sendable { case origin case movable case interactivity + case provider case workingDirectories case changesets case backgroundWork @@ -1931,6 +1939,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 +1962,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 +2006,10 @@ public struct ChatSummary: Codable, Sendable { /// Absence defaults to {@link ChatInteractivity.Full} for backward /// compatibility. public var interactivity: ChatInteractivity? + /// Agent provider ID backing this chat. See {@link ChatState.provider}. + /// + /// 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 +2024,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 +2036,7 @@ public struct ChatSummary: Codable, Sendable { self.origin = origin self.movable = movable self.interactivity = interactivity + self.provider = provider self.workingDirectories = workingDirectories } } @@ -2587,6 +2603,10 @@ 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. See {@link ChatState.provider}. + /// + /// 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 +2628,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 +2636,7 @@ public struct SessionChatSummary: Codable, Sendable { self.title = title self.origin = origin self.interactivity = interactivity + self.provider = provider self.status = status self.changes = changes } @@ -2670,12 +2692,23 @@ 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'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 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( + schema: SessionConfigSchema? = nil, values: [String: AnyCodable] ) { + self.schema = schema self.values = values } } 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/20261014-chat-config.json b/docs/.changes/20261014-chat-config.json index 77e9350c9..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` 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` 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/.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/.changes/20261019-kotlin-positional-chat-fields.json b/docs/.changes/20261019-kotlin-positional-chat-fields.json new file mode 100644 index 000000000..126588623 --- /dev/null +++ b/docs/.changes/20261019-kotlin-positional-chat-fields.json @@ -0,0 +1,7 @@ +{ + "type": "changed", + "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 new file mode 100644 index 000000000..b240aff09 --- /dev/null +++ b/docs/.changes/20261020-rust-chat-struct-literals.json @@ -0,0 +1,7 @@ +{ + "type": "changed", + "message": "Direct Rust struct literals for `ChatState`, `ChatSummary`, and `SessionChatSummary` must now include `provider: None`.", + "targets": [ + "rust" + ] +} 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 408af695b..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) @@ -218,7 +219,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 296b55d54..11eccd26f 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -192,9 +192,10 @@ 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) + config?: ChatConfigState // chat's chatMutable values (+ optional schema) turns: Turn[] // completed turns turnsNextCursor?: string // page older turns via fetchTurns @@ -212,13 +213,15 @@ 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` 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 +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. 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..4d63a2a4c 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 @@ -78,20 +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; 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`. +[`ChatState.config`](/reference/chat#chatstate) holds the chat's own values for +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. 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 +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` 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. + +### Chat providers + +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, but `provider` is relative to its session, so `chat/moved` may set or +clear it. + +Hosts that advertise +[`multipleChatProviders`](/reference/common#initializeresult) accept a +`provider` on `createChat`, letting one session mix agents. Forks and side +chats continue the source chat's history, so they always use the source chat's +agent. ## Relationship to the session channel @@ -114,6 +131,7 @@ 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 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 }`, @@ -235,6 +253,17 @@ 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 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. + +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. 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 The host validates and persists ownership, its internal hierarchy, catalog diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 12beee22c..37553a92e 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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -248,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`, `provider`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ @@ -1710,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 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" @@ -1730,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.", @@ -2669,6 +2698,9 @@ { "$ref": "#/$defs/ChatConfigChangedAction" }, + { + "$ref": "#/$defs/ChatMovedAction" + }, { "$ref": "#/$defs/ChatInputRequestedAction" }, @@ -3907,6 +3939,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. See {@link ChatState.provider}.\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." @@ -4045,7 +4081,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": [ @@ -5220,6 +5256,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. 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", "items": { @@ -5250,7 +5290,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", @@ -5336,6 +5376,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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -5476,12 +5520,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.", + "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", + "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": "Config values set on this chat, keyed by property id" + "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": [ @@ -9462,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 a6443cfd0..b8ab3a3a2 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 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": [ @@ -193,6 +197,11 @@ "snapshots" ] }, + "MultipleChatProvidersCapability": { + "type": "object", + "description": "Presence capability for {@link InitializeResult.multipleChatProviders}.\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.", @@ -1452,6 +1461,10 @@ "title": { "type": "string", "description": "Client-selected initial display title for the new chat." + }, + "provider": { + "type": "string", + "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": [ @@ -1496,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\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", @@ -3219,6 +3232,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. See {@link ChatState.provider}.\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." @@ -3357,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": [ @@ -4532,6 +4549,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. 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", "items": { @@ -4562,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", @@ -4648,6 +4669,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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -4788,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.", + "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", + "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": "Config values set on this chat, keyed by property id" + "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": [ @@ -8232,6 +8261,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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -8240,7 +8273,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`, `provider`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ @@ -9702,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 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" @@ -9722,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.", @@ -10753,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 c62e88b4e..58d9577ad 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -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. See {@link ChatState.provider}.\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." @@ -1459,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": [ @@ -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 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", "items": { @@ -2664,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", @@ -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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -2890,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.", + "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", + "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": "Config values set on this chat, keyed by property id" + "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": [ @@ -6279,6 +6295,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 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": [ @@ -6287,6 +6307,11 @@ "snapshots" ] }, + "MultipleChatProvidersCapability": { + "type": "object", + "description": "Presence capability for {@link InitializeResult.multipleChatProviders}.\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.", @@ -7546,6 +7571,10 @@ "title": { "type": "string", "description": "Client-selected initial display title for the new chat." + }, + "provider": { + "type": "string", + "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": [ @@ -7590,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\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", @@ -8976,6 +9005,9 @@ { "$ref": "#/$defs/ChatConfigChangedAction" }, + { + "$ref": "#/$defs/ChatMovedAction" + }, { "$ref": "#/$defs/ChatInputRequestedAction" }, @@ -9422,6 +9454,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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -9430,7 +9466,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`, `provider`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ @@ -10758,7 +10794,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 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" @@ -10778,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 cce8cecd5..ad27dabb4 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -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. See {@link ChatState.provider}.\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." @@ -1761,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": [ @@ -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 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", "items": { @@ -2966,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", @@ -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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -3192,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.", + "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", + "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": "Config values set on this chat, keyed by property id" + "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 13ebffafe..4d750087e 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -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. See {@link ChatState.provider}.\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." @@ -1370,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": [ @@ -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 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", "items": { @@ -2575,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", @@ -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. See {@link ChatState.provider}.\n\nStability: 1.0 - Early development." + }, "workingDirectories": { "type": "array", "items": { @@ -2801,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.", + "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", + "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": "Config values set on this chat, keyed by property id" + "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/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index c51def8ff..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' }, @@ -2312,6 +2313,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 fdefaa7d0..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' }, @@ -1804,6 +1805,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 27bc3b3b7..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' }, @@ -1788,6 +1789,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 3469643d0..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'); @@ -1830,6 +1831,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 26294f73a..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' }, @@ -1710,6 +1711,7 @@ const COMMAND_STRUCTS = [ 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', 'AutomationCustomizationsCapability', + 'MultipleChatProvidersCapability', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'ChannelDeliveryResumeParams', 'ChannelDeliveryResumeOptions', 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/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 ab109f881..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, @@ -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 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 * @version 1 @@ -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 381cd2ae7..dd2068ed9 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -120,6 +120,18 @@ export interface CreateChatParams extends BaseParams { workingDirectories?: URI[]; /** Client-selected initial display title for the new chat. */ title?: string; + /** + * 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` 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; } // ─── moveChat ──────────────────────────────────────────────────────────────── @@ -189,6 +201,19 @@ export type ChatMoveDestination = * Rejection leaves ownership, catalog order, chat state, and root summaries * unchanged. * + * 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. + * + * 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. 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 * `session/chatRemoved`, `session/chatAdded`, `session/chatsReordered`, and 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 4455a1cb9..ac66855da 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'; @@ -82,6 +83,16 @@ export interface ChatState { * compatibility. */ interactivity?: ChatInteractivity; + /** + * Agent provider ID backing this chat. Absent means the session's + * {@link SessionMetadata.provider | `provider`}; hosts set it when it + * 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; /** * The subset of the session's * {@link SessionState.workingDirectories | `workingDirectories`} that this @@ -127,9 +138,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; @@ -223,6 +232,11 @@ export interface ChatSummary { * compatibility. */ interactivity?: ChatInteractivity; + /** + * Agent provider ID backing this chat. See {@link ChatState.provider}. + * @stability 1.0 + */ + provider?: string; /** * The subset of the session's working directories this chat uses. * See {@link ChatState.workingDirectories} for the full semantics. @@ -417,18 +431,33 @@ 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. `values` holds the chat's `chatMutable` properties + * 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: a chat's `values` change only through + * `chat/configChanged` on the chat. * - * 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. + * 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 values set on this chat, keyed by property id */ + /** + * 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 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/channels-session/actions.ts b/types/channels-session/actions.ts index 9e54a3319..21098a881 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -95,7 +95,7 @@ export interface SessionChatUpdatedAction { /** * Mutable summary fields that changed; omitted fields are unchanged. * - * Identity fields (`resource`) never change and MUST be omitted by + * 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/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/channels-session/state.ts b/types/channels-session/state.ts index 9c36015ca..e9eae9935 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -543,6 +543,11 @@ export interface SessionChatSummary { * backward compatibility. */ interactivity?: ChatInteractivity; + /** + * Agent provider ID backing this chat. See {@link ChatState.provider}. + * @stability 1.0 + */ + provider?: string; /** * Current chat status, matching {@link ChatSummary.status}. * @@ -622,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/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/common/commands.ts b/types/common/commands.ts index f978aac92..12590b93a 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -313,8 +313,24 @@ export interface InitializeResult { * @see {@link /specification/tcp-channel | TCP Channel} */ 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. + * @stability 1.0 + * @see {@link /specification/chat-channel#chat-providers | Chat Channel: Chat providers} + */ + multipleChatProviders?: MultipleChatProvidersCapability; } +/** + * Presence capability for {@link InitializeResult.multipleChatProviders}. + * + * @category Commands + * @stability 1.0 + */ +export interface MultipleChatProvidersCapability {} + /** * Automation features supported by this host authority. * 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/316-session-chatupdated-ignores-provider.json b/types/test-cases/reducers/316-session-chatupdated-ignores-provider.json new file mode 100644 index 000000000..89015f480 --- /dev/null +++ b/types/test-cases/reducers/316-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/317-chat-configchanged-replace-creates-config-when-absent.json b/types/test-cases/reducers/317-chat-configchanged-replace-creates-config-when-absent.json new file mode 100644 index 000000000..650a2c6fb --- /dev/null +++ b/types/test-cases/reducers/317-chat-configchanged-replace-creates-config-when-absent.json @@ -0,0 +1,34 @@ +{ + "description": "chat/configChanged with replace creates the chat's 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/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/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/056-chat-config-snapshot.json b/types/test-cases/round-trips/056-chat-config-snapshot.json index c22144f61..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 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 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", diff --git a/types/test-cases/round-trips/079-create-chat-provider.json b/types/test-cases/round-trips/079-create-chat-provider.json new file mode 100644 index 000000000..f92b21975 --- /dev/null +++ b/types/test-cases/round-trips/079-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/080-initialize-multiple-chat-providers.json b/types/test-cases/round-trips/080-initialize-multiple-chat-providers.json new file mode 100644 index 000000000..a133fc678 --- /dev/null +++ b/types/test-cases/round-trips/080-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/081-chat-summary-provider.json b/types/test-cases/round-trips/081-chat-summary-provider.json new file mode 100644 index 000000000..e08f03b6d --- /dev/null +++ b/types/test-cases/round-trips/081-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" + } + } + ] +} diff --git a/types/test-cases/round-trips/082-session-chat-summary-provider-round-trip.json b/types/test-cases/round-trips/082-session-chat-summary-provider-round-trip.json new file mode 100644 index 000000000..73a0870f9 --- /dev/null +++ b/types/test-cases/round-trips/082-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" + } + ] +} 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 + } + ] +} 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',