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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ changes accumulate. Track in-flight protocol changes via PRs touching

Spec version: `0.3.0`

- Added `status` and `error` to `ChangesetOperation` and a new
`changeset/operationStatusChanged` action so servers can reflect an
operation's execution lifecycle (`idle → running → error`) back into
changeset state.
- Renamed the `UserMessage` type to `Message` and surfaced it consistently
across turn state (`Turn.message`, `ActiveTurn.message`, `PendingMessage.message`)
and the actions that carry it (`session/turnStarted`,
Expand Down
5 changes: 5 additions & 0 deletions clients/go/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file.

## [Unreleased]

### Added
- `status` and `error` fields on `ChangesetOperation` and the
`changeset/operationStatusChanged` action, tracking the
`idle → running → error` lifecycle of a changeset operation.

## [0.1.0] — 2026-05-28

Implements AHP `0.2.0`.
Expand Down
28 changes: 28 additions & 0 deletions clients/go/ahptypes/actions.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const (
ActionTypeChangesetFileSet ActionType = "changeset/fileSet"
ActionTypeChangesetFileRemoved ActionType = "changeset/fileRemoved"
ActionTypeChangesetOperationsChanged ActionType = "changeset/operationsChanged"
ActionTypeChangesetOperationStatusChanged ActionType = "changeset/operationStatusChanged"
ActionTypeChangesetCleared ActionType = "changeset/cleared"
ActionTypeRootTerminalsChanged ActionType = "root/terminalsChanged"
ActionTypeRootConfigChanged ActionType = "root/configChanged"
Expand Down Expand Up @@ -722,6 +723,26 @@ type ChangesetOperationsChangedAction struct {
Operations []ChangesetOperation `json:"operations,omitempty"`
}

// The {@link ChangesetOperation.status} for a single operation transitioned
// (e.g. `idle → running → idle`, or `running → error`). The error payload
// is set together with `status` whenever it transitions to
// {@link ChangesetOperationStatus.Error | Error}, and cleared on any other
// transition.
//
// Targets one operation by its {@link ChangesetOperation.id}. If no
// operation with that id is currently present in the changeset, the action
// is a no-op. Use {@link ChangesetOperationsChangedAction} to add, remove,
// or otherwise replace the operation list itself.
type ChangesetOperationStatusChangedAction struct {
Type ActionType `json:"type"`
// The {@link ChangesetOperation.id} whose status changed.
OperationId string `json:"operationId"`
// New execution status.
Status ChangesetOperationStatus `json:"status"`
// Cause when `status === ChangesetOperationStatus.Error`; otherwise omitted.
Error *ErrorInfo `json:"error,omitempty"`
}

// Drop every file from the changeset.
//
// Two cases use this:
Expand Down Expand Up @@ -942,6 +963,7 @@ func (*ChangesetStatusChangedAction) isStateAction() {}
func (*ChangesetFileSetAction) isStateAction() {}
func (*ChangesetFileRemovedAction) isStateAction() {}
func (*ChangesetOperationsChangedAction) isStateAction() {}
func (*ChangesetOperationStatusChangedAction) isStateAction() {}
func (*ChangesetClearedAction) isStateAction() {}
func (*RootTerminalsChangedAction) isStateAction() {}
func (*TerminalDataAction) isStateAction() {}
Expand Down Expand Up @@ -1253,6 +1275,12 @@ func (u *StateAction) UnmarshalJSON(data []byte) error {
return err
}
u.Value = &value
case "changeset/operationStatusChanged":
var value ChangesetOperationStatusChangedAction
if err := json.Unmarshal(data, &value); err != nil {
return err
}
u.Value = &value
case "changeset/cleared":
var value ChangesetClearedAction
if err := json.Unmarshal(data, &value); err != nil {
Expand Down
32 changes: 32 additions & 0 deletions clients/go/ahptypes/state.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,25 @@ const (
ChangesetStatusError ChangesetStatus = "error"
)

// Execution lifecycle of a {@link ChangesetOperation}.
//
// An operation is invoked imperatively via `invokeChangesetOperation`, but
// its progress and outcome are reflected back into changeset state so that
// every subscriber observes a consistent view (e.g. a spinner on a "Create
// Pull Request" button, or an inline error after a failed "revert").
type ChangesetOperationStatus string

const (
// The operation is ready to be invoked. This is the default when
// {@link ChangesetOperation.status} is omitted.
ChangesetOperationStatusIdle ChangesetOperationStatus = "idle"
// An invocation of this operation is currently in flight.
ChangesetOperationStatusRunning ChangesetOperationStatus = "running"
// The most recent invocation failed. The cause is described by
// {@link ChangesetOperation.error}.
ChangesetOperationStatusError ChangesetOperationStatus = "error"
)

// Where a {@link ChangesetOperation} can be invoked.
type ChangesetOperationScope string

Expand Down Expand Up @@ -2079,6 +2098,19 @@ type ChangesetOperation struct {
Confirmation *StringOrMarkdown `json:"confirmation,omitempty"`
// Optional generic icon hint, e.g. `"check"`, `"trash"`.
Icon *string `json:"icon,omitempty"`
// Current execution status. The server sets
// {@link ChangesetOperationStatus.Running | Running} while an invocation
// is in flight, {@link ChangesetOperationStatus.Error | Error} when the
// most recent invocation failed, and
// {@link ChangesetOperationStatus.Idle | Idle} otherwise.
//
// Clients SHOULD reflect this state in the UI — e.g. disabling the
// control or showing a spinner while `Running`, and surfacing
// {@link error} while `Error`.
Status ChangesetOperationStatus `json:"status"`
// Cause of failure. Present iff
// `status === ChangesetOperationStatus.Error`; otherwise omitted.
Error *ErrorInfo `json:"error,omitempty"`
}

// OTLP telemetry channels the agent host emits.
Expand Down
5 changes: 5 additions & 0 deletions clients/kotlin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump

## [Unreleased]

### Added
- `status` and `error` fields on `ChangesetOperation` and the
`changeset/operationStatusChanged` action, tracking the
`idle → running → error` lifecycle of a changeset operation.

## [0.2.0] — 2026-05-28

Implements AHP `0.2.0`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.microsoft.agenthostprotocol.generated.AgentSelection
import com.microsoft.agenthostprotocol.generated.ChangesetFile
import com.microsoft.agenthostprotocol.generated.ChangesetState
import com.microsoft.agenthostprotocol.generated.ChangesetStatus
import com.microsoft.agenthostprotocol.generated.ChangesetOperationStatus
import com.microsoft.agenthostprotocol.generated.ChildCustomization
import com.microsoft.agenthostprotocol.generated.ChildCustomizationAgent
import com.microsoft.agenthostprotocol.generated.ChildCustomizationHook
Expand Down Expand Up @@ -48,6 +49,7 @@ import com.microsoft.agenthostprotocol.generated.StateActionChangesetCleared
import com.microsoft.agenthostprotocol.generated.StateActionChangesetFileRemoved
import com.microsoft.agenthostprotocol.generated.StateActionChangesetFileSet
import com.microsoft.agenthostprotocol.generated.StateActionChangesetOperationsChanged
import com.microsoft.agenthostprotocol.generated.StateActionChangesetOperationStatusChanged
import com.microsoft.agenthostprotocol.generated.StateActionChangesetStatusChanged
import com.microsoft.agenthostprotocol.generated.StateActionRootActiveSessionsChanged
import com.microsoft.agenthostprotocol.generated.StateActionRootAgentsChanged
Expand Down Expand Up @@ -1305,6 +1307,26 @@ public fun changesetReducer(state: ChangesetState, action: StateAction): Changes
is StateActionChangesetOperationsChanged ->
state.copy(operations = action.value.operations)

is StateActionChangesetOperationStatusChanged -> {
val operations = state.operations
val idx = operations?.indexOfFirst { it.id == action.value.operationId } ?: -1
if (operations == null || idx < 0) {
state
} else {
// Carry `error` only when the new status is `Error` so we don't
// leave a stale error on an operation that recovered or started
// running.
val current = operations[idx]
val nextOp = if (action.value.status == ChangesetOperationStatus.ERROR) {
current.copy(status = action.value.status, error = action.value.error)
} else {
current.copy(status = action.value.status, error = null)
}
val next = operations.toMutableList().also { it[idx] = nextOp }
state.copy(operations = next)
}
}

is StateActionChangesetCleared ->
if (state.files.isEmpty()) state else state.copy(files = emptyList())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ enum class ActionType {
CHANGESET_FILE_REMOVED,
@SerialName("changeset/operationsChanged")
CHANGESET_OPERATIONS_CHANGED,
@SerialName("changeset/operationStatusChanged")
CHANGESET_OPERATION_STATUS_CHANGED,
@SerialName("changeset/cleared")
CHANGESET_CLEARED,
@SerialName("root/terminalsChanged")
Expand Down Expand Up @@ -832,6 +834,23 @@ data class ChangesetOperationsChangedAction(
val operations: List<ChangesetOperation>? = null
)

@Serializable
data class ChangesetOperationStatusChangedAction(
val type: ActionType,
/**
* The {@link ChangesetOperation.id} whose status changed.
*/
val operationId: String,
/**
* New execution status.
*/
val status: ChangesetOperationStatus,
/**
* Cause when `status === ChangesetOperationStatus.Error`; otherwise omitted.
*/
val error: ErrorInfo? = null
)

@Serializable
data class ChangesetClearedAction(
val type: ActionType
Expand Down Expand Up @@ -1042,6 +1061,7 @@ sealed interface StateAction
@JvmInline value class StateActionChangesetFileSet(val value: ChangesetFileSetAction) : StateAction
@JvmInline value class StateActionChangesetFileRemoved(val value: ChangesetFileRemovedAction) : StateAction
@JvmInline value class StateActionChangesetOperationsChanged(val value: ChangesetOperationsChangedAction) : StateAction
@JvmInline value class StateActionChangesetOperationStatusChanged(val value: ChangesetOperationStatusChangedAction) : StateAction
@JvmInline value class StateActionChangesetCleared(val value: ChangesetClearedAction) : StateAction
@JvmInline value class StateActionRootTerminalsChanged(val value: RootTerminalsChangedAction) : StateAction
@JvmInline value class StateActionRootConfigChanged(val value: RootConfigChangedAction) : StateAction
Expand Down Expand Up @@ -1118,6 +1138,7 @@ internal object StateActionSerializer : KSerializer<StateAction> {
"changeset/fileSet" -> StateActionChangesetFileSet(input.json.decodeFromJsonElement(ChangesetFileSetAction.serializer(), element))
"changeset/fileRemoved" -> StateActionChangesetFileRemoved(input.json.decodeFromJsonElement(ChangesetFileRemovedAction.serializer(), element))
"changeset/operationsChanged" -> StateActionChangesetOperationsChanged(input.json.decodeFromJsonElement(ChangesetOperationsChangedAction.serializer(), element))
"changeset/operationStatusChanged" -> StateActionChangesetOperationStatusChanged(input.json.decodeFromJsonElement(ChangesetOperationStatusChangedAction.serializer(), element))
"changeset/cleared" -> StateActionChangesetCleared(input.json.decodeFromJsonElement(ChangesetClearedAction.serializer(), element))
"root/terminalsChanged" -> StateActionRootTerminalsChanged(input.json.decodeFromJsonElement(RootTerminalsChangedAction.serializer(), element))
"root/configChanged" -> StateActionRootConfigChanged(input.json.decodeFromJsonElement(RootConfigChangedAction.serializer(), element))
Expand Down Expand Up @@ -1187,6 +1208,7 @@ internal object StateActionSerializer : KSerializer<StateAction> {
is StateActionChangesetFileSet -> output.json.encodeToJsonElement(ChangesetFileSetAction.serializer(), value.value)
is StateActionChangesetFileRemoved -> output.json.encodeToJsonElement(ChangesetFileRemovedAction.serializer(), value.value)
is StateActionChangesetOperationsChanged -> output.json.encodeToJsonElement(ChangesetOperationsChangedAction.serializer(), value.value)
is StateActionChangesetOperationStatusChanged -> output.json.encodeToJsonElement(ChangesetOperationStatusChangedAction.serializer(), value.value)
is StateActionChangesetCleared -> output.json.encodeToJsonElement(ChangesetClearedAction.serializer(), value.value)
is StateActionRootTerminalsChanged -> output.json.encodeToJsonElement(RootTerminalsChangedAction.serializer(), value.value)
is StateActionRootConfigChanged -> output.json.encodeToJsonElement(RootConfigChangedAction.serializer(), value.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,35 @@ enum class ChangesetStatus {
ERROR
}

/**
* Execution lifecycle of a {@link ChangesetOperation}.
*
* An operation is invoked imperatively via `invokeChangesetOperation`, but
* its progress and outcome are reflected back into changeset state so that
* every subscriber observes a consistent view (e.g. a spinner on a "Create
* Pull Request" button, or an inline error after a failed "revert").
*/
@Serializable
enum class ChangesetOperationStatus {
/**
* The operation is ready to be invoked. This is the default when
* {@link ChangesetOperation.status} is omitted.
*/
@SerialName("idle")
IDLE,
/**
* An invocation of this operation is currently in flight.
*/
@SerialName("running")
RUNNING,
/**
* The most recent invocation failed. The cause is described by
* {@link ChangesetOperation.error}.
*/
@SerialName("error")
ERROR
}

/**
* Where a {@link ChangesetOperation} can be invoked.
*/
Expand Down Expand Up @@ -2994,7 +3023,24 @@ data class ChangesetOperation(
/**
* Optional generic icon hint, e.g. `"check"`, `"trash"`.
*/
val icon: String? = null
val icon: String? = null,
/**
* Current execution status. The server sets
* {@link ChangesetOperationStatus.Running | Running} while an invocation
* is in flight, {@link ChangesetOperationStatus.Error | Error} when the
* most recent invocation failed, and
* {@link ChangesetOperationStatus.Idle | Idle} otherwise.
*
* Clients SHOULD reflect this state in the UI — e.g. disabling the
* control or showing a spinner while `Running`, and surfacing
* {@link error} while `Error`.
*/
val status: ChangesetOperationStatus,
/**
* Cause of failure. Present iff
* `status === ChangesetOperationStatus.Error`; otherwise omitted.
*/
val error: ErrorInfo? = null
)

@Serializable
Expand Down
5 changes: 5 additions & 0 deletions clients/rust/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ matching `## [X.Y.Z]` heading is missing from this file.

## [Unreleased]

### Added
- `status` and `error` fields on `ChangesetOperation` and the
`changeset/operationStatusChanged` action, tracking the
`idle → running → error` lifecycle of a changeset operation.

## [0.2.0] — 2026-05-28

Implements AHP `0.2.0`. Bumps the `ahp-types`, `ahp`, and `ahp-ws` crates
Expand Down
37 changes: 32 additions & 5 deletions clients/rust/crates/ahp-types/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};

use crate::state::{
AgentInfo, AgentSelection, ChangesetFile, ChangesetOperation, ChangesetStatus,
ChangesetSummary, ConfirmationOption, Customization, ErrorInfo, Message, ModelSelection,
PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputAnswer, SessionInputRequest,
SessionInputResponseKind, TerminalClaim, TerminalInfo, ToolCallCancellationReason,
ToolCallConfirmationReason, ToolCallResult, ToolDefinition, ToolResultContent, UsageInfo,
AgentInfo, AgentSelection, ChangesetFile, ChangesetOperation, ChangesetOperationStatus,
ChangesetStatus, ChangesetSummary, ConfirmationOption, Customization, ErrorInfo, Message,
ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputAnswer,
SessionInputRequest, SessionInputResponseKind, TerminalClaim, TerminalInfo,
ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallResult, ToolDefinition,
ToolResultContent, UsageInfo,
};

// ─── ActionType ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -116,6 +117,8 @@ pub enum ActionType {
ChangesetFileRemoved,
#[serde(rename = "changeset/operationsChanged")]
ChangesetOperationsChanged,
#[serde(rename = "changeset/operationStatusChanged")]
ChangesetOperationStatusChanged,
#[serde(rename = "changeset/cleared")]
ChangesetCleared,
#[serde(rename = "root/terminalsChanged")]
Expand Down Expand Up @@ -886,6 +889,28 @@ pub struct ChangesetOperationsChangedAction {
pub operations: Option<Vec<ChangesetOperation>>,
}

/// The {@link ChangesetOperation.status} for a single operation transitioned
/// (e.g. `idle → running → idle`, or `running → error`). The error payload
/// is set together with `status` whenever it transitions to
/// {@link ChangesetOperationStatus.Error | Error}, and cleared on any other
/// transition.
///
/// Targets one operation by its {@link ChangesetOperation.id}. If no
/// operation with that id is currently present in the changeset, the action
/// is a no-op. Use {@link ChangesetOperationsChangedAction} to add, remove,
/// or otherwise replace the operation list itself.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChangesetOperationStatusChangedAction {
/// The {@link ChangesetOperation.id} whose status changed.
pub operation_id: String,
/// New execution status.
pub status: ChangesetOperationStatus,
/// Cause when `status === ChangesetOperationStatus.Error`; otherwise omitted.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorInfo>,
}

/// Drop every file from the changeset.
///
/// Two cases use this:
Expand Down Expand Up @@ -1162,6 +1187,8 @@ pub enum StateAction {
ChangesetFileRemoved(ChangesetFileRemovedAction),
#[serde(rename = "changeset/operationsChanged")]
ChangesetOperationsChanged(ChangesetOperationsChangedAction),
#[serde(rename = "changeset/operationStatusChanged")]
ChangesetOperationStatusChanged(ChangesetOperationStatusChangedAction),
#[serde(rename = "changeset/cleared")]
ChangesetCleared(ChangesetClearedAction),
#[serde(rename = "root/terminalsChanged")]
Expand Down
Loading
Loading