Skip to content
Draft
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
3 changes: 3 additions & 0 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,9 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
}
}
return ReduceOutcomeNoOp
case *ahptypes.SessionStateReplacedAction:
*state = a.State
return ReduceOutcomeApplied
case *ahptypes.SessionCustomizationsChangedAction:
state.Customizations = append([]ahptypes.Customization(nil), a.Customizations...)
return ReduceOutcomeApplied
Expand Down
19 changes: 19 additions & 0 deletions clients/go/ahptypes/actions.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
ActionTypeRootActiveSessionsChanged ActionType = "root/activeSessionsChanged"
ActionTypeSessionReady ActionType = "session/ready"
ActionTypeSessionCreationFailed ActionType = "session/creationFailed"
ActionTypeSessionStateReplaced ActionType = "session/stateReplaced"
ActionTypeSessionChatAdded ActionType = "session/chatAdded"
ActionTypeSessionChatRemoved ActionType = "session/chatRemoved"
ActionTypeSessionChatUpdated ActionType = "session/chatUpdated"
Expand Down Expand Up @@ -972,6 +973,17 @@ type SessionInputNeededRemovedAction struct {
Id string `json:"id"`
}

// The authoritative state for this session was replaced while the channel
// remained subscribed.
//
// Full-replacement semantics: the supplied state replaces the previous session
// state entirely.
type SessionStateReplacedAction struct {
Type ActionType `json:"type"`
// Complete authoritative state for the session.
State SessionState `json:"state"`
}

// The session's customizations have changed.
//
// Full-replacement semantics: the `customizations` array replaces the
Expand Down Expand Up @@ -1541,6 +1553,7 @@ func (*ChatWorkingDirectorySetAction) isStateAction() {}
func (*ChatWorkingDirectoryRemovedAction) isStateAction() {}
func (*SessionInputNeededSetAction) isStateAction() {}
func (*SessionInputNeededRemovedAction) isStateAction() {}
func (*SessionStateReplacedAction) isStateAction() {}
func (*SessionCustomizationsChangedAction) isStateAction() {}
func (*SessionCustomizationToggledAction) isStateAction() {}
func (*SessionCustomizationUpdatedAction) isStateAction() {}
Expand Down Expand Up @@ -1891,6 +1904,12 @@ func (u *StateAction) UnmarshalJSON(data []byte) error {
return err
}
u.Value = &value
case "session/stateReplaced":
var value SessionStateReplacedAction
if err := json.Unmarshal(data, &value); err != nil {
return err
}
u.Value = &value
case "session/customizationsChanged":
var value SessionCustomizationsChangedAction
if err := json.Unmarshal(data, &value); err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,8 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat
}
}

is StateActionSessionStateReplaced -> action.value.state

is StateActionSessionCustomizationsChanged -> state.copy(customizations = action.value.customizations)

is StateActionSessionCustomizationToggled -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ enum class ActionType {
SESSION_READY,
@SerialName("session/creationFailed")
SESSION_CREATION_FAILED,
@SerialName("session/stateReplaced")
SESSION_STATE_REPLACED,
@SerialName("session/chatAdded")
SESSION_CHAT_ADDED,
@SerialName("session/chatRemoved")
Expand Down Expand Up @@ -1045,6 +1047,15 @@ data class ChatInputCompletedAction(
val answers: Map<String, ChatInputAnswer>? = null
)

@Serializable
data class SessionStateReplacedAction(
val type: ActionType,
/**
* Complete authoritative state for the session.
*/
val state: SessionState
)

@Serializable
data class SessionCustomizationsChangedAction(
val type: ActionType,
Expand Down Expand Up @@ -1584,6 +1595,7 @@ sealed interface 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
@JvmInline value class StateActionSessionStateReplaced(val value: SessionStateReplacedAction) : StateAction
@JvmInline value class StateActionSessionCustomizationsChanged(val value: SessionCustomizationsChangedAction) : StateAction
@JvmInline value class StateActionSessionCustomizationToggled(val value: SessionCustomizationToggledAction) : StateAction
@JvmInline value class StateActionSessionCustomizationUpdated(val value: SessionCustomizationUpdatedAction) : StateAction
Expand Down Expand Up @@ -1684,6 +1696,7 @@ internal object StateActionSerializer : KSerializer<StateAction> {
"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))
"session/stateReplaced" -> StateActionSessionStateReplaced(input.json.decodeFromJsonElement(SessionStateReplacedAction.serializer(), element))
"session/customizationsChanged" -> StateActionSessionCustomizationsChanged(input.json.decodeFromJsonElement(SessionCustomizationsChangedAction.serializer(), element))
"session/customizationToggled" -> StateActionSessionCustomizationToggled(input.json.decodeFromJsonElement(SessionCustomizationToggledAction.serializer(), element))
"session/customizationUpdated" -> StateActionSessionCustomizationUpdated(input.json.decodeFromJsonElement(SessionCustomizationUpdatedAction.serializer(), element))
Expand Down Expand Up @@ -1777,6 +1790,7 @@ internal object StateActionSerializer : KSerializer<StateAction> {
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)
is StateActionSessionStateReplaced -> output.json.encodeToJsonElement(SessionStateReplacedAction.serializer(), value.value)
is StateActionSessionCustomizationsChanged -> output.json.encodeToJsonElement(SessionCustomizationsChangedAction.serializer(), value.value)
is StateActionSessionCustomizationToggled -> output.json.encodeToJsonElement(SessionCustomizationToggledAction.serializer(), value.value)
is StateActionSessionCustomizationUpdated -> output.json.encodeToJsonElement(SessionCustomizationUpdatedAction.serializer(), value.value)
Expand Down
22 changes: 19 additions & 3 deletions clients/rust/crates/ahp-types/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ use crate::state::{
ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary,
ConfirmationOption, Customization, ErrorInfo, McpAuthRequirement, McpServerState, Message,
ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest,
SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason,
ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment,
ToolDefinition, ToolResultContent, Turn, UsageInfo,
SessionState, SideChatSelection, TerminalClaim, TerminalInfo, TextRange,
ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult,
ToolCallRiskAssessment, ToolDefinition, ToolResultContent, Turn, UsageInfo,
};

// ─── ActionType ──────────────────────────────────────────────────────
Expand All @@ -36,6 +36,8 @@ pub enum ActionType {
SessionReady,
#[serde(rename = "session/creationFailed")]
SessionCreationFailed,
#[serde(rename = "session/stateReplaced")]
SessionStateReplaced,
#[serde(rename = "session/chatAdded")]
SessionChatAdded,
#[serde(rename = "session/chatRemoved")]
Expand Down Expand Up @@ -1146,6 +1148,18 @@ pub struct ChatInputCompletedAction {
pub answers: Option<std::collections::HashMap<String, ChatInputAnswer>>,
}

/// The authoritative state for this session was replaced while the channel
/// remained subscribed.
///
/// Full-replacement semantics: the supplied state replaces the previous session
/// state entirely.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionStateReplacedAction {
/// Complete authoritative state for the session.
pub state: SessionState,
}

/// The session's customizations have changed.
///
/// Full-replacement semantics: the `customizations` array replaces the
Expand Down Expand Up @@ -1874,6 +1888,8 @@ pub enum StateAction {
ChatInputAnswerChanged(ChatInputAnswerChangedAction),
#[serde(rename = "chat/inputCompleted")]
ChatInputCompleted(ChatInputCompletedAction),
#[serde(rename = "session/stateReplaced")]
SessionStateReplaced(Box<SessionStateReplacedAction>),
#[serde(rename = "session/customizationsChanged")]
SessionCustomizationsChanged(SessionCustomizationsChangedAction),
#[serde(rename = "session/customizationToggled")]
Expand Down
4 changes: 4 additions & 0 deletions clients/rust/crates/ahp/src/reducers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,10 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) -
state.status = new_status;
ReduceOutcome::Applied
}
StateAction::SessionStateReplaced(a) => {
*state = a.state.clone();
ReduceOutcome::Applied
}
StateAction::SessionCustomizationsChanged(a) => {
state.customizations = Some(a.customizations.clone());
ReduceOutcome::Applied
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum ActionType: String, Codable, Sendable {
case rootActiveSessionsChanged = "root/activeSessionsChanged"
case sessionReady = "session/ready"
case sessionCreationFailed = "session/creationFailed"
case sessionStateReplaced = "session/stateReplaced"
case sessionChatAdded = "session/chatAdded"
case sessionChatRemoved = "session/chatRemoved"
case sessionChatUpdated = "session/chatUpdated"
Expand Down Expand Up @@ -1338,6 +1339,20 @@ public struct ChatInputCompletedAction: Codable, Sendable {
}
}

public struct SessionStateReplacedAction: Codable, Sendable {
public var type: ActionType
/// Complete authoritative state for the session.
public var state: SessionState

public init(
type: ActionType,
state: SessionState
) {
self.type = type
self.state = state
}
}

public struct SessionCustomizationsChangedAction: Codable, Sendable {
public var type: ActionType
/// Updated customization list (full replacement).
Expand Down Expand Up @@ -2064,6 +2079,7 @@ public enum StateAction: Codable, Sendable {
case chatInputRequested(ChatInputRequestedAction)
case chatInputAnswerChanged(ChatInputAnswerChangedAction)
case chatInputCompleted(ChatInputCompletedAction)
case sessionStateReplaced(SessionStateReplacedAction)
case sessionCustomizationsChanged(SessionCustomizationsChangedAction)
case sessionCustomizationToggled(SessionCustomizationToggledAction)
case sessionCustomizationUpdated(SessionCustomizationUpdatedAction)
Expand Down Expand Up @@ -2208,6 +2224,8 @@ public enum StateAction: Codable, Sendable {
self = .chatInputAnswerChanged(try ChatInputAnswerChangedAction(from: decoder))
case "chat/inputCompleted":
self = .chatInputCompleted(try ChatInputCompletedAction(from: decoder))
case "session/stateReplaced":
self = .sessionStateReplaced(try SessionStateReplacedAction(from: decoder))
case "session/customizationsChanged":
self = .sessionCustomizationsChanged(try SessionCustomizationsChangedAction(from: decoder))
case "session/customizationToggled":
Expand Down Expand Up @@ -2338,6 +2356,7 @@ public enum StateAction: Codable, Sendable {
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)
case .sessionStateReplaced(let v): try v.encode(to: encoder)
case .sessionCustomizationsChanged(let v): try v.encode(to: encoder)
case .sessionCustomizationToggled(let v): try v.encode(to: encoder)
case .sessionCustomizationUpdated(let v): try v.encode(to: encoder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,9 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS

// ── Customizations ──────────────────────────────────────────────────

case .sessionStateReplaced(let a):
return a.state

case .sessionCustomizationsChanged(let a):
var next = state
next.customizations = a.customizations
Expand Down
4 changes: 4 additions & 0 deletions docs/.changes/20260731-session-state-replaced.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "added",
"message": "`session/stateReplaced` action for replacing session state while its channel remains subscribed."
}
20 changes: 20 additions & 0 deletions schema/actions.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,23 @@
"error"
]
},
"SessionStateReplacedAction": {
"type": "object",
"description": "The authoritative state for this session was replaced while the channel\nremained subscribed.\n\nFull-replacement semantics: the supplied state replaces the previous session\nstate entirely.",
"properties": {
"type": {
"const": "session/stateReplaced"
},
"state": {
"$ref": "#/$defs/SessionState",
"description": "Complete authoritative state for the session."
}
},
"required": [
"type",
"state"
]
},
"SessionChatAddedAction": {
"type": "object",
"description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.",
Expand Down Expand Up @@ -7526,6 +7543,9 @@
{
"$ref": "#/$defs/SessionCreationFailedAction"
},
{
"$ref": "#/$defs/SessionStateReplacedAction"
},
{
"$ref": "#/$defs/SessionChatAddedAction"
},
Expand Down
20 changes: 20 additions & 0 deletions schema/commands.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6397,6 +6397,23 @@
"error"
]
},
"SessionStateReplacedAction": {
"type": "object",
"description": "The authoritative state for this session was replaced while the channel\nremained subscribed.\n\nFull-replacement semantics: the supplied state replaces the previous session\nstate entirely.",
"properties": {
"type": {
"const": "session/stateReplaced"
},
"state": {
"$ref": "#/$defs/SessionState",
"description": "Complete authoritative state for the session."
}
},
"required": [
"type",
"state"
]
},
"SessionChatAddedAction": {
"type": "object",
"description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.",
Expand Down Expand Up @@ -8357,6 +8374,9 @@
{
"$ref": "#/$defs/SessionCreationFailedAction"
},
{
"$ref": "#/$defs/SessionStateReplacedAction"
},
{
"$ref": "#/$defs/SessionChatAddedAction"
},
Expand Down
20 changes: 20 additions & 0 deletions schema/errors.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6952,6 +6952,9 @@
{
"$ref": "#/$defs/SessionCreationFailedAction"
},
{
"$ref": "#/$defs/SessionStateReplacedAction"
},
{
"$ref": "#/$defs/SessionChatAddedAction"
},
Expand Down Expand Up @@ -7413,6 +7416,23 @@
"error"
]
},
"SessionStateReplacedAction": {
"type": "object",
"description": "The authoritative state for this session was replaced while the channel\nremained subscribed.\n\nFull-replacement semantics: the supplied state replaces the previous session\nstate entirely.",
"properties": {
"type": {
"const": "session/stateReplaced"
},
"state": {
"$ref": "#/$defs/SessionState",
"description": "Complete authoritative state for the session."
}
},
"required": [
"type",
"state"
]
},
"SessionChatAddedAction": {
"type": "object",
"description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.",
Expand Down
1 change: 1 addition & 0 deletions scripts/generate-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,7 @@ const ACTION_VARIANTS: {
{ type: 'chat/workingDirectoryRemoved', variantName: 'ChatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' },
{ type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' },
{ type: 'session/inputNeededRemoved', variantName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' },
{ type: 'session/stateReplaced', variantName: 'SessionStateReplaced', tsInterface: 'SessionStateReplacedAction' },
{ type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' },
{ type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' },
{ type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' },
Expand Down
1 change: 1 addition & 0 deletions scripts/generate-kotlin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,7 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[]
{ type: 'chat/inputRequested', caseName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' },
{ type: 'chat/inputAnswerChanged', caseName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' },
{ type: 'chat/inputCompleted', caseName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' },
{ type: 'session/stateReplaced', caseName: 'SessionStateReplaced', tsInterface: 'SessionStateReplacedAction' },
{ type: 'session/customizationsChanged', caseName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' },
{ type: 'session/customizationToggled', caseName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' },
{ type: 'session/customizationUpdated', caseName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' },
Expand Down
Loading
Loading