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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ changes accumulate. Track in-flight protocol changes via PRs touching
extends `PaginatedResult`, letting clients fetch a large session catalogue
incrementally. Fully additive — omitting the fields preserves today's
behaviour.
- `SubscribeParams.view.turns`, `ChatState.turnsNextCursor`, and the
`chat/turnsLoaded` action so clients can subscribe to a bounded tail of chat
history and page older turns into the reduced chat state on demand.

### Changed

- `fetchTurns` now accepts `cursor` from `ChatState.turnsNextCursor` and returns
an empty result after the host has loaded older turns into chat state, instead
of returning a detached `{ turns, hasMore }` page.
- Generated clients now advertise only protocol `0.5.1`, since the `fetchTurns`
contract is not wire-compatible with `0.5.0`.

### Removed

Expand Down
11 changes: 11 additions & 0 deletions clients/go/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file.
`ListSessionsParams` and `ListSessionsResult` now carry these fields, letting
clients page through a large session catalogue. Fully additive — omitting the
fields preserves prior behaviour.
- `SubscribeParams.View.Turns`, `ChatState.TurnsNextCursor`, and the
`chat/turnsLoaded` action so clients can subscribe to a bounded tail of chat
history and page older turns into the reduced chat state on demand.
- `SessionState.InputNeeded` — a session-level aggregate of outstanding input
requests across all chats (`SessionInputRequest` union with
`SessionChatInputRequest`, `SessionToolConfirmationRequest`, and
Expand All @@ -42,6 +45,14 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file.
- Optional `Model` and `Tools` fields on `AgentCustomization` for a custom
agent's pinned model and tool allowlist.

### Changed

- `fetchTurns` now accepts `Cursor` from `ChatState.TurnsNextCursor` and returns
an empty result after the host has loaded older turns into chat state, instead
of returning a detached `{ turns, hasMore }` page.
- Generated clients now advertise only protocol `0.5.1`, since the `fetchTurns`
contract is not wire-compatible with `0.5.0`.

### Removed

- `Filter` field from `ListSessionsParams`. It was an untyped placeholder with
Expand Down
15 changes: 15 additions & 0 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,20 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R
})
case *ahptypes.ChatTruncatedAction:
return applyTruncated(state, a.TurnId)
case *ahptypes.ChatTurnsLoadedAction:
existingIds := make(map[string]struct{}, len(state.Turns))
for _, turn := range state.Turns {
existingIds[turn.Id] = struct{}{}
}
olderTurns := make([]ahptypes.Turn, 0, len(a.Turns)+len(state.Turns))
for _, turn := range a.Turns {
if _, ok := existingIds[turn.Id]; !ok {
olderTurns = append(olderTurns, turn)
}
}
state.Turns = append(olderTurns, state.Turns...)
state.TurnsNextCursor = a.TurnsNextCursor
return ReduceOutcomeApplied
case *ahptypes.ChatInputRequestedAction:
upsertInputRequest(state, a.Request)
return ReduceOutcomeApplied
Expand Down Expand Up @@ -1173,6 +1187,7 @@ func applyMcpServerStatusChanged(state *ahptypes.SessionState, a *ahptypes.Sessi
func applyTruncated(state *ahptypes.ChatState, turnID *string) ReduceOutcome {
if turnID == nil {
state.Turns = []ahptypes.Turn{}
state.TurnsNextCursor = nil
} else {
idx := -1
for i := range state.Turns {
Expand Down
23 changes: 23 additions & 0 deletions clients/go/ahptypes/actions.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const (
ActionTypeSessionCustomizationRemoved ActionType = "session/customizationRemoved"
ActionTypeSessionMcpServerStateChanged ActionType = "session/mcpServerStateChanged"
ActionTypeChatTruncated ActionType = "chat/truncated"
ActionTypeChatTurnsLoaded ActionType = "chat/turnsLoaded"
ActionTypeSessionIsReadChanged ActionType = "session/isReadChanged"
ActionTypeSessionIsArchivedChanged ActionType = "session/isArchivedChanged"
ActionTypeSessionActivityChanged ActionType = "session/activityChanged"
Expand Down Expand Up @@ -673,6 +674,21 @@ type ChatTruncatedAction struct {
TurnId *string `json:"turnId,omitempty"`
}

// Loads older completed turns into this chat's state.
//
// Hosts dispatch this before responding to `fetchTurns`, and before applying
// any operation that references a turn older than the currently loaded window.
// `turns` is ordered oldest-first and is prepended to the current `turns`
// window. `turnsNextCursor` replaces the state's cursor; omit it when all
// retained turns are now loaded.
type ChatTurnsLoadedAction struct {
Type ActionType `json:"type"`
// Older completed turns loaded into the state, ordered oldest-first.
Turns []Turn `json:"turns"`
// Opaque cursor for loading the next older page, if one remains.
TurnsNextCursor *string `json:"turnsNextCursor,omitempty"`
}

// The read state of the session changed.
//
// Dispatched by a client to mark a session as read (e.g. after viewing it)
Expand Down Expand Up @@ -1278,6 +1294,7 @@ func (*ChatInputRequestedAction) isStateAction() {}
func (*ChatInputAnswerChangedAction) isStateAction() {}
func (*ChatInputCompletedAction) isStateAction() {}
func (*ChatTruncatedAction) isStateAction() {}
func (*ChatTurnsLoadedAction) isStateAction() {}
func (*SessionIsReadChangedAction) isStateAction() {}
func (*SessionIsArchivedChangedAction) isStateAction() {}
func (*SessionActivityChangedAction) isStateAction() {}
Expand Down Expand Up @@ -1538,6 +1555,12 @@ func (u *StateAction) UnmarshalJSON(data []byte) error {
return err
}
u.Value = &value
case "chat/turnsLoaded":
var value ChatTurnsLoadedAction
if err := json.Unmarshal(data, &value); err != nil {
return err
}
u.Value = &value
case "session/isReadChanged":
var value SessionIsReadChangedAction
if err := json.Unmarshal(data, &value); err != nil {
Expand Down
44 changes: 34 additions & 10 deletions clients/go/ahptypes/commands.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,24 @@ type SubscribeParams struct {
// updates while preserving the same reduced state. Omit this field for the
// server's default delivery behavior.
Delivery *SubscriptionDeliveryOptions `json:"delivery,omitempty"`
// Optional client-requested shape for the returned snapshot.
//
// Servers that do not understand a requested view ignore it and return their
// default snapshot. Clients MUST tolerate receiving more state than requested.
View *SubscribeView `json:"view,omitempty"`
}

// Optional client-requested shape for a subscription snapshot.
type SubscribeView struct {
// Advisory number of most-recent completed turns to expose in a chat
// snapshot.
//
// Servers MAY return more or fewer turns than requested. When omitted, the
// host MUST return all retained turns. When older turns remain available, the
// returned {@link ChatState} carries `turnsNextCursor`; clients pass that
// cursor to `fetchTurns` to ask the host to page more turns into the chat
// state.
Turns *int64 `json:"turns,omitempty"`
}

// Advisory delivery preferences for a single subscription.
Expand Down Expand Up @@ -683,23 +701,29 @@ type CreateResourceWatchResult struct {
Channel URI `json:"channel"`
}

// Fetches historical turns for a chat. Used for lazy loading of conversation
// history.
// Requests that the host load older historical turns into a chat state.
//
// The command result does not carry turns. Instead, before responding, the host
// MUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat
// channel's `turns` state, ahead of the already-loaded window, and update or
// clear `turnsNextCursor`.
//
// Before applying any operation that references a turn outside the currently
// loaded window, the host MUST eagerly load enough older turns into state for
// that operation to reduce against valid state.
type FetchTurnsParams struct {
// Channel URI this command targets.
Channel URI `json:"channel"`
// Turn ID to fetch before (exclusive). Omit to fetch from the most recent turn.
Before *string `json:"before,omitempty"`
// Maximum number of turns to return. Server MAY impose its own upper bound.
Limit *int64 `json:"limit,omitempty"`
// Opaque cursor from `ChatState.turnsNextCursor`.
//
// The host MUST reject unrecognised cursors with `InvalidParams`. Omit only
// when asking the host to opportunistically load its next older page for the
// chat, if any.
Cursor *string `json:"cursor,omitempty"`
}

// Result of the `fetchTurns` command.
type FetchTurnsResult struct {
// The requested turns, ordered oldest-first
Turns []Turn `json:"turns"`
// Whether more turns exist before the returned range
HasMore bool `json:"hasMore"`
}

// Stop receiving updates for a channel.
Expand Down
7 changes: 7 additions & 0 deletions clients/go/ahptypes/state.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,13 @@ type ChatState struct {
WorkingDirectory *URI `json:"workingDirectory,omitempty"`
// Completed turns
Turns []Turn `json:"turns"`
// Cursor for loading older completed turns into this chat state.
//
// Presence means `turns` is a tail window and more historical turns are
// available. Pass this opaque cursor to `fetchTurns`; the host MUST insert
// the loaded turns into state and update or clear this cursor before
// responding. Absence means the state contains all retained turns.
TurnsNextCursor *string `json:"turnsNextCursor,omitempty"`
// Currently in-progress turn
ActiveTurn *ActiveTurn `json:"activeTurn,omitempty"`
// Message to inject into the current turn at a convenient point
Expand Down
1 change: 0 additions & 1 deletion clients/go/ahptypes/version.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const ProtocolVersion = "0.5.1"
// shared backing array.
var supportedProtocolVersions = []string{
"0.5.1",
"0.5.0",
}

// SupportedProtocolVersions returns every protocol version this client
Expand Down
3 changes: 1 addition & 2 deletions clients/go/release-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"client": "go",
"packageVersion": "0.5.0",
"supportedProtocolVersions": [
"0.5.1",
"0.5.0"
"0.5.1"
]
}
11 changes: 11 additions & 0 deletions clients/kotlin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump
`ListSessionsParams` and `ListSessionsResult` now carry these fields, letting
clients page through a large session catalogue. Fully additive — omitting the
fields preserves prior behaviour.
- `SubscribeParams.view.turns`, `ChatState.turnsNextCursor`, and the
`chat/turnsLoaded` action so clients can subscribe to a bounded tail of chat
history and page older turns into the reduced chat state on demand.
- `SessionState.inputNeeded` — a session-level aggregate of outstanding input
requests across all chats (`SessionInputRequest` sealed interface with
`SessionChatInputRequest`, `SessionToolConfirmationRequest`, and
Expand All @@ -41,6 +44,14 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump
- Optional `model` and `tools` fields on `AgentCustomization` for a custom
agent's pinned model and tool allowlist.

### Changed

- `fetchTurns` now accepts `cursor` from `ChatState.turnsNextCursor` and returns
an empty result after the host has loaded older turns into chat state, instead
of returning a detached `{ turns, hasMore }` page.
- Generated clients now advertise only protocol `0.5.1`, since the `fetchTurns`
contract is not wire-compatible with `0.5.0`.

### Removed

- `filter` field from `ListSessionsParams`. It was an untyped placeholder with
Expand Down
3 changes: 1 addition & 2 deletions clients/kotlin/release-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"client": "kotlin",
"packageVersion": "0.5.0",
"supportedProtocolVersions": [
"0.5.1",
"0.5.0"
"0.5.1"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -1054,13 +1054,24 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when
}
val next = state.copy(
turns = turns,
turnsNextCursor = if (a.turnId == null) null else state.turnsNextCursor,
activeTurn = null,
inputRequests = null,
modifiedAt = nowIsoString(),
)
next.copy(status = chatSummaryStatus(next))
}

is StateActionChatTurnsLoaded -> {
val a = action.value
val existingIds = state.turns.map { it.id }.toSet()
val olderTurns = a.turns.filter { it.id !in existingIds }
state.copy(
turns = olderTurns + state.turns,
turnsNextCursor = a.turnsNextCursor,
)
}

// ── Session Input Requests ────────────────────────────────────────────

is StateActionChatInputRequested ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ enum class ActionType {
SESSION_MCP_SERVER_STATE_CHANGED,
@SerialName("chat/truncated")
CHAT_TRUNCATED,
@SerialName("chat/turnsLoaded")
CHAT_TURNS_LOADED,
@SerialName("session/isReadChanged")
SESSION_IS_READ_CHANGED,
@SerialName("session/isArchivedChanged")
Expand Down Expand Up @@ -968,6 +970,19 @@ data class ChatTruncatedAction(
val turnId: String? = null
)

@Serializable
data class ChatTurnsLoadedAction(
val type: ActionType,
/**
* Older completed turns loaded into the state, ordered oldest-first.
*/
val turns: List<Turn>,
/**
* Opaque cursor for loading the next older page, if one remains.
*/
val turnsNextCursor: String? = null
)

@Serializable
data class SessionConfigChangedAction(
val type: ActionType,
Expand Down Expand Up @@ -1398,6 +1413,7 @@ sealed interface StateAction
@JvmInline value class StateActionSessionCustomizationRemoved(val value: SessionCustomizationRemovedAction) : StateAction
@JvmInline value class StateActionSessionMcpServerStateChanged(val value: SessionMcpServerStateChangedAction) : StateAction
@JvmInline value class StateActionChatTruncated(val value: ChatTruncatedAction) : StateAction
@JvmInline value class StateActionChatTurnsLoaded(val value: ChatTurnsLoadedAction) : StateAction
@JvmInline value class StateActionSessionConfigChanged(val value: SessionConfigChangedAction) : StateAction
@JvmInline value class StateActionSessionMetaChanged(val value: SessionMetaChangedAction) : StateAction
@JvmInline value class StateActionChangesetStatusChanged(val value: ChangesetStatusChangedAction) : StateAction
Expand Down Expand Up @@ -1488,6 +1504,7 @@ internal object StateActionSerializer : KSerializer<StateAction> {
"session/customizationRemoved" -> StateActionSessionCustomizationRemoved(input.json.decodeFromJsonElement(SessionCustomizationRemovedAction.serializer(), element))
"session/mcpServerStateChanged" -> StateActionSessionMcpServerStateChanged(input.json.decodeFromJsonElement(SessionMcpServerStateChangedAction.serializer(), element))
"chat/truncated" -> StateActionChatTruncated(input.json.decodeFromJsonElement(ChatTruncatedAction.serializer(), element))
"chat/turnsLoaded" -> StateActionChatTurnsLoaded(input.json.decodeFromJsonElement(ChatTurnsLoadedAction.serializer(), element))
"session/configChanged" -> StateActionSessionConfigChanged(input.json.decodeFromJsonElement(SessionConfigChangedAction.serializer(), element))
"session/metaChanged" -> StateActionSessionMetaChanged(input.json.decodeFromJsonElement(SessionMetaChangedAction.serializer(), element))
"changeset/statusChanged" -> StateActionChangesetStatusChanged(input.json.decodeFromJsonElement(ChangesetStatusChangedAction.serializer(), element))
Expand Down Expand Up @@ -1571,6 +1588,7 @@ internal object StateActionSerializer : KSerializer<StateAction> {
is StateActionSessionCustomizationRemoved -> output.json.encodeToJsonElement(SessionCustomizationRemovedAction.serializer(), value.value)
is StateActionSessionMcpServerStateChanged -> output.json.encodeToJsonElement(SessionMcpServerStateChangedAction.serializer(), value.value)
is StateActionChatTruncated -> output.json.encodeToJsonElement(ChatTruncatedAction.serializer(), value.value)
is StateActionChatTurnsLoaded -> output.json.encodeToJsonElement(ChatTurnsLoadedAction.serializer(), value.value)
is StateActionSessionConfigChanged -> output.json.encodeToJsonElement(SessionConfigChangedAction.serializer(), value.value)
is StateActionSessionMetaChanged -> output.json.encodeToJsonElement(SessionMetaChangedAction.serializer(), value.value)
is StateActionChangesetStatusChanged -> output.json.encodeToJsonElement(ChangesetStatusChangedAction.serializer(), value.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,29 @@ data class SubscribeParams(
* updates while preserving the same reduced state. Omit this field for the
* server's default delivery behavior.
*/
val delivery: SubscriptionDeliveryOptions? = null
val delivery: SubscriptionDeliveryOptions? = null,
/**
* Optional client-requested shape for the returned snapshot.
*
* Servers that do not understand a requested view ignore it and return their
* default snapshot. Clients MUST tolerate receiving more state than requested.
*/
val view: SubscribeView? = null
)

@Serializable
data class SubscribeView(
/**
* Advisory number of most-recent completed turns to expose in a chat
* snapshot.
*
* Servers MAY return more or fewer turns than requested. When omitted, the
* host MUST return all retained turns. When older turns remain available, the
* returned {@link ChatState} carries `turnsNextCursor`; clients pass that
* cursor to `fetchTurns` to ask the host to page more turns into the chat
* state.
*/
val turns: Long? = null
)

@Serializable
Expand Down Expand Up @@ -755,26 +777,17 @@ data class FetchTurnsParams(
*/
val channel: String,
/**
* Turn ID to fetch before (exclusive). Omit to fetch from the most recent turn.
*/
val before: String? = null,
/**
* Maximum number of turns to return. Server MAY impose its own upper bound.
* Opaque cursor from `ChatState.turnsNextCursor`.
*
* The host MUST reject unrecognised cursors with `InvalidParams`. Omit only
* when asking the host to opportunistically load its next older page for the
* chat, if any.
*/
val limit: Long? = null
val cursor: String? = null
)

@Serializable
data class FetchTurnsResult(
/**
* The requested turns, ordered oldest-first
*/
val turns: List<Turn>,
/**
* Whether more turns exist before the returned range
*/
val hasMore: Boolean
)
class FetchTurnsResult

@Serializable
data class UnsubscribeParams(
Expand Down
Loading
Loading