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',