Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -1180,7 +1182,7 @@ public sealed record SessionChatUpdatedAction

/// <summary>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.</summary>
public required PartialChatSummary Changes { get; init; }
}
Expand Down Expand Up @@ -2092,10 +2094,10 @@ public sealed record ChatIsArchivedChangedAction
/// <summary>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.</summary>
/// {@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.</summary>
public sealed record ChatConfigChangedAction
{
public ActionType Type { get; init; } = ActionType.ChatConfigChanged;
Expand All @@ -2108,6 +2110,31 @@ public sealed record ChatConfigChangedAction
public bool? Replace { get; init; }
}

/// <summary>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.</summary>
public sealed record ChatMovedAction
{
public ActionType Type { get; init; } = ActionType.ChatMoved;

/// <summary>The new owning session URI.</summary>
public required string Session { get; init; }

/// <summary>The chat's provider in the new session; absent clears it.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Provider { get; init; }

/// <summary>The chat's config schema in the new session; absent clears it.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public SessionConfigSchema? Schema { get; init; }
}

/// <summary>A session requested input from the user.
///
/// Creates an unresolved {@link InputRequestResponsePart} in the active turn,
Expand Down Expand Up @@ -2782,6 +2809,12 @@ public sealed record PartialChatSummary
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ChatInteractivity? Interactivity { get; init; }

/// <summary>Agent provider ID backing this chat. See {@link ChatState.provider}.
///
/// Stability: 1.0 - Early development.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Provider { get; init; }

/// <summary>The subset of the session's working directories this chat uses.
/// See {@link ChatState.workingDirectories} for the full semantics.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,14 @@ public sealed record InitializeResult
/// Stability: 1.0 - Early development.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public TcpConnectionsCapability? TcpConnections { get; init; }

/// <summary>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.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public MultipleChatProvidersCapability? MultipleChatProviders { get; init; }
}

/// <summary>Identifies a protocol implementation — the software (and build) on one end
Expand Down Expand Up @@ -614,6 +622,13 @@ public sealed record AutomationCustomizationsCapability
{
}

/// <summary>Presence capability for {@link InitializeResult.multipleChatProviders}.
///
/// Stability: 1.0 - Early development.</summary>
public sealed record MultipleChatProvidersCapability
{
}

/// <summary>Re-establishes a dropped connection. The server replays missed actions or
/// provides fresh snapshots.</summary>
public sealed record ReconnectParams
Expand Down Expand Up @@ -1015,6 +1030,19 @@ public sealed record CreateChatParams
/// <summary>Client-selected initial display title for the new chat.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Title { get; init; }

/// <summary>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.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Provider { get; init; }
}

/// <summary>Disposes a chat and cleans up server-side resources.</summary>
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2365,6 +2365,12 @@ public sealed class ChatSummary
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ChatInteractivity? Interactivity { get; set; }

/// <summary>Agent provider ID backing this chat. See {@link ChatState.provider}.
///
/// Stability: 1.0 - Early development.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Provider { get; set; }

/// <summary>The subset of the session's working directories this chat uses.
/// See {@link ChatState.workingDirectories} for the full semantics.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
Expand Down Expand Up @@ -2492,6 +2498,17 @@ public sealed class ChatState
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ChatInteractivity? Interactivity { get; set; }

/// <summary>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.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Provider { get; set; }

/// <summary>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
Expand Down Expand Up @@ -2536,9 +2553,7 @@ public sealed class ChatState
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<CanvasReference>? Canvases { get; set; }

/// <summary>Configuration values set on this chat, applied over the owning session's
/// {@link SessionState.config | `SessionState.config`}. See
/// {@link ChatConfigState}.</summary>
/// <summary>This chat's configuration. See {@link ChatConfigState}.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ChatConfigState? Config { get; set; }

Expand Down Expand Up @@ -3343,6 +3358,12 @@ public sealed record SessionChatSummary
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ChatInteractivity? Interactivity { get; init; }

/// <summary>Agent provider ID backing this chat. See {@link ChatState.provider}.
///
/// Stability: 1.0 - Early development.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Provider { get; init; }

/// <summary>Current chat status, matching {@link ChatSummary.status}.
///
/// Includes the activity bits and the orthogonal {@link SessionStatus.IsRead}
Expand Down Expand Up @@ -3464,8 +3485,8 @@ public sealed record SessionConfigPropertySchema
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? SessionMutable { get; init; }

/// <summary>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.</summary>
/// <summary>When `true`, the property may be changed per chat with
/// `chat/configChanged`, affecting only that chat.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? ChatMutable { get; init; }
}
Expand Down Expand Up @@ -3497,16 +3518,30 @@ public sealed class SessionConfigState
public required Dictionary<string, JsonElement> Values { get; set; }
}

/// <summary>Configuration values set on a chat: an overlay on the owning session's
/// {@link SessionState.config | `SessionState.config`}.
/// <summary>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.</summary>
/// 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.</summary>
public sealed class ChatConfigState
{
/// <summary>Config values set on this chat, keyed by property id</summary>
/// <summary>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.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public SessionConfigSchema? Schema { get; set; }

/// <summary>The chat's values for the `chatMutable` properties of the effective
/// schema, keyed by property id. A missing key takes that property's
/// `default`.</summary>
public required Dictionary<string, JsonElement> Values { get; set; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions clients/dotnet/src/AgentHostProtocol/Reducers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, JsonElement>() };
}
return ReduceOutcome.Applied;
}

return ReduceOutcome.OutOfScope;
Expand Down
8 changes: 8 additions & 0 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
37 changes: 32 additions & 5 deletions clients/go/ahptypes/actions.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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() {}
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading