From 885d8e5863b24cdd59efb93eb7689bf82386a2c7 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 1 Jul 2026 14:22:32 -0700 Subject: [PATCH 1/2] Add cursor-based pagination to listSessions Introduce optional cursor + limit pagination for the root `listSessions` command so clients with a large session catalogue can load it incrementally instead of receiving every `SessionSummary` in one response. Modeled on MCP's opaque-cursor pagination, plus an optional `limit` the server SHOULD respect (but MAY cap). The pagination inputs/outputs are factored into reusable `PaginatedParams` (`limit` + `cursor`) and `PaginatedResult` (`nextCursor`) base interfaces in `types/common/commands.ts`, carrying the canonical pagination contract in one place for future paginated commands. `ListSessionsParams` now extends `PaginatedParams` and `ListSessionsResult` extends `PaginatedResult`. Fully additive: a client that omits `limit`/`cursor` and ignores `nextCursor` sees today's behaviour, and a server that does not paginate returns everything in one page. Regenerates the schema and all five client mirrors, updates the root channel spec doc, and adds CHANGELOG entries to the spec plus every client. Closes #275 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 ++ clients/go/CHANGELOG.md | 5 ++ clients/go/ahptypes/commands.generated.go | 23 ++++++- clients/kotlin/CHANGELOG.md | 5 ++ .../generated/Commands.generated.kt | 22 ++++++- clients/rust/CHANGELOG.md | 5 ++ clients/rust/crates/ahp-types/src/commands.rs | 26 +++++++- clients/rust/crates/ahp/src/hosts/runtime.rs | 2 + .../Generated/Commands.generated.swift | 22 ++++++- clients/swift/CHANGELOG.md | 5 ++ clients/typescript/CHANGELOG.md | 5 ++ docs/specification/root-channel.md | 12 ++++ schema/commands.schema.json | 40 +++++++++++- schema/errors.schema.json | 40 +++++++++++- scripts/generate-go.ts | 2 + scripts/generate-kotlin.ts | 2 + scripts/generate-rust.ts | 2 + scripts/generate-swift.ts | 2 + types/channels-root/commands.ts | 34 +++++++++-- types/common/commands.ts | 61 +++++++++++++++++++ types/index.ts | 2 + 21 files changed, 311 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc897eb..d54a5d44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ changes accumulate. Track in-flight protocol changes via PRs touching - Optional `capabilities` field on `AgentInfo` (`AgentCapabilities` with a nested `multipleChats` capability carrying `fork`) so clients gate multi-chat and fork via advertised capabilities instead of provider-id switches. +- Cursor-based pagination for `listSessions`, via new shared `PaginatedParams` + (`limit` + `cursor`) and `PaginatedResult` (`nextCursor`) types: + `ListSessionsParams` now extends `PaginatedParams` and `ListSessionsResult` + extends `PaginatedResult`, letting clients fetch a large session catalogue + incrementally. Fully additive — omitting the fields preserves today's + behaviour. ## [0.5.1] — Unreleased diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index b1a4b216..2b3bf92a 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -22,6 +22,11 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. - Optional `capabilities` field on `AgentInfo` (`AgentCapabilities` with a nested `multipleChats` capability carrying `fork`) so clients gate multi-chat and fork via advertised capabilities instead of provider-id switches. +- Cursor-based pagination for `listSessions`, via new shared `PaginatedParams` + (`Limit` + `Cursor`) and `PaginatedResult` (`NextCursor`) types: + `ListSessionsParams` and `ListSessionsResult` now carry these fields, letting + clients page through a large session catalogue. Fully additive — omitting the + fields preserves prior behaviour. - `SessionState.InputNeeded` — a session-level aggregate of outstanding input requests across all chats (`SessionInputRequest` union with `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 70be4320..287c0ccb 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -313,16 +313,37 @@ type DisposeChatParams struct { // The session list is **not** part of the state tree because it can be arbitrarily // large. Clients fetch it imperatively and maintain a local cache updated by // `root/sessionAdded` and `root/sessionRemoved` notifications. +// +// A large catalogue can be fetched incrementally via the {@link PaginatedParams} +// `limit`/`cursor` inputs (see that type for the full pagination contract). The +// server SHOULD return most-recently-modified entries first, so the first page +// is the immediately useful one. The `root/session*` notifications keep an +// already-fetched page live; pagination governs only the initial and backfill +// fetches. type ListSessionsParams struct { // Channel URI this command targets. Channel URI `json:"channel"` + // Maximum number of entries to return in this page. The server SHOULD respect + // this bound but MAY return fewer entries and MAY impose its own upper cap. + // Omit to let the server choose the page size. + Limit *int64 `json:"limit,omitempty"` + // Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + // Omit to fetch the first page. Cursors are server-defined and MUST be treated + // as opaque — do not parse, modify, or persist them across connections. An + // unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + Cursor *string `json:"cursor,omitempty"` // Optional filter criteria Filter *json.RawMessage `json:"filter,omitempty"` } // Result of the `listSessions` command. type ListSessionsResult struct { - // The list of session summaries. + // Opaque cursor for the next page. Present when more entries exist beyond the + // returned page; absent signals the end of the collection. Pass it back as + // {@link PaginatedParams.cursor} to fetch the following page. + NextCursor *string `json:"nextCursor,omitempty"` + // The list of session summaries. The server SHOULD order them + // most-recently-modified first. Items []SessionSummary `json:"items"` } diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 28757e74..c675e460 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -22,6 +22,11 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump - Optional `capabilities` field on `AgentInfo` (`AgentCapabilities` with a nested `multipleChats` capability carrying `fork`) so clients gate multi-chat and fork via advertised capabilities instead of provider-id switches. +- Cursor-based pagination for `listSessions`, via new shared `PaginatedParams` + (`limit` + `cursor`) and `PaginatedResult` (`nextCursor`) types: + `ListSessionsParams` and `ListSessionsResult` now carry these fields, letting + clients page through a large session catalogue. Fully additive — omitting the + fields preserves prior behaviour. - `SessionState.inputNeeded` — a session-level aggregate of outstanding input requests across all chats (`SessionInputRequest` sealed interface with `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and 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 75d26873..80666736 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 @@ -398,6 +398,19 @@ data class ListSessionsParams( * Channel URI this command targets. */ val channel: String, + /** + * Maximum number of entries to return in this page. The server SHOULD respect + * this bound but MAY return fewer entries and MAY impose its own upper cap. + * Omit to let the server choose the page size. + */ + val limit: Long? = null, + /** + * Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + * Omit to fetch the first page. Cursors are server-defined and MUST be treated + * as opaque — do not parse, modify, or persist them across connections. An + * unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + */ + val cursor: String? = null, /** * Optional filter criteria */ @@ -407,7 +420,14 @@ data class ListSessionsParams( @Serializable data class ListSessionsResult( /** - * The list of session summaries. + * Opaque cursor for the next page. Present when more entries exist beyond the + * returned page; absent signals the end of the collection. Pass it back as + * {@link PaginatedParams.cursor} to fetch the following page. + */ + val nextCursor: String? = null, + /** + * The list of session summaries. The server SHOULD order them + * most-recently-modified first. */ val items: List ) diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 23366ed7..8c6ae18f 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -23,6 +23,11 @@ matching `## [X.Y.Z]` heading is missing from this file. - Optional `capabilities` field on `AgentInfo` (`AgentCapabilities` with a nested `multipleChats` capability carrying `fork`) so clients gate multi-chat and fork via advertised capabilities instead of provider-id switches. +- Cursor-based pagination for `listSessions`, via new shared `PaginatedParams` + (`limit` + `cursor`) and `PaginatedResult` (`next_cursor`) types: + `ListSessionsParams` and `ListSessionsResult` now carry these fields, letting + clients page through a large session catalogue. Fully additive — omitting the + fields preserves prior behaviour. - `SessionState.input_needed` — a session-level aggregate of outstanding input requests across all chats (`SessionInputRequest` enum with `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 7ce8f58a..a8c34e8c 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -392,11 +392,29 @@ pub struct DisposeChatParams { /// The session list is **not** part of the state tree because it can be arbitrarily /// large. Clients fetch it imperatively and maintain a local cache updated by /// `root/sessionAdded` and `root/sessionRemoved` notifications. +/// +/// A large catalogue can be fetched incrementally via the {@link PaginatedParams} +/// `limit`/`cursor` inputs (see that type for the full pagination contract). The +/// server SHOULD return most-recently-modified entries first, so the first page +/// is the immediately useful one. The `root/session*` notifications keep an +/// already-fetched page live; pagination governs only the initial and backfill +/// fetches. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListSessionsParams { /// Channel URI this command targets. pub channel: Uri, + /// Maximum number of entries to return in this page. The server SHOULD respect + /// this bound but MAY return fewer entries and MAY impose its own upper cap. + /// Omit to let the server choose the page size. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + /// Omit to fetch the first page. Cursors are server-defined and MUST be treated + /// as opaque — do not parse, modify, or persist them across connections. An + /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cursor: Option, /// Optional filter criteria #[serde(default, skip_serializing_if = "Option::is_none")] pub filter: Option, @@ -406,7 +424,13 @@ pub struct ListSessionsParams { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListSessionsResult { - /// The list of session summaries. + /// Opaque cursor for the next page. Present when more entries exist beyond the + /// returned page; absent signals the end of the collection. Pass it back as + /// {@link PaginatedParams.cursor} to fetch the following page. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// The list of session summaries. The server SHOULD order them + /// most-recently-modified first. pub items: Vec, } diff --git a/clients/rust/crates/ahp/src/hosts/runtime.rs b/clients/rust/crates/ahp/src/hosts/runtime.rs index b1912525..795a1a2c 100644 --- a/clients/rust/crates/ahp/src/hosts/runtime.rs +++ b/clients/rust/crates/ahp/src/hosts/runtime.rs @@ -318,6 +318,8 @@ impl HostRuntime { ListSessionsParams { channel: ROOT_RESOURCE_URI.to_string(), filter: None, + limit: None, + cursor: None, }, ) .await; diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 417cece4..6c646b8f 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -395,25 +395,45 @@ public struct DisposeChatParams: Codable, Sendable { public struct ListSessionsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Maximum number of entries to return in this page. The server SHOULD respect + /// this bound but MAY return fewer entries and MAY impose its own upper cap. + /// Omit to let the server choose the page size. + public var limit: Int? + /// Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + /// Omit to fetch the first page. Cursors are server-defined and MUST be treated + /// as opaque — do not parse, modify, or persist them across connections. An + /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + public var cursor: String? /// Optional filter criteria public var filter: AnyCodable? public init( channel: String, + limit: Int? = nil, + cursor: String? = nil, filter: AnyCodable? = nil ) { self.channel = channel + self.limit = limit + self.cursor = cursor self.filter = filter } } public struct ListSessionsResult: Codable, Sendable { - /// The list of session summaries. + /// Opaque cursor for the next page. Present when more entries exist beyond the + /// returned page; absent signals the end of the collection. Pass it back as + /// {@link PaginatedParams.cursor} to fetch the following page. + public var nextCursor: String? + /// The list of session summaries. The server SHOULD order them + /// most-recently-modified first. public var items: [SessionSummary] public init( + nextCursor: String? = nil, items: [SessionSummary] ) { + self.nextCursor = nextCursor self.items = items } } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index e1d7ea5b..f4f02d3e 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -25,6 +25,11 @@ the tag matches the version pinned in [`VERSION`](VERSION). - Optional `capabilities` field on `AgentInfo` (`AgentCapabilities` with a nested `multipleChats` capability carrying `fork`) so clients gate multi-chat and fork via advertised capabilities instead of provider-id switches. +- Cursor-based pagination for `listSessions`, via new shared `PaginatedParams` + (`limit` + `cursor`) and `PaginatedResult` (`nextCursor`) types: + `ListSessionsParams` and `ListSessionsResult` now carry these fields, letting + clients page through a large session catalogue. Fully additive — omitting the + fields preserves prior behaviour. - `SessionState.inputNeeded` — a session-level aggregate of outstanding input requests across all chats (`SessionInputRequest` enum with `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 1dccdc9a..e171dd78 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -28,6 +28,11 @@ hotfix escape hatch. - Optional `capabilities` field on `AgentInfo` (`AgentCapabilities` with a nested `multipleChats` capability carrying `fork`) so clients gate multi-chat and fork via advertised capabilities instead of provider-id switches. +- Cursor-based pagination for `listSessions`, via new shared `PaginatedParams` + (`limit` + `cursor`) and `PaginatedResult` (`nextCursor`) types: + `ListSessionsParams` and `ListSessionsResult` now carry these fields, letting + clients page through a large session catalogue. Fully additive — omitting the + fields preserves prior behaviour. - `SessionState.inputNeeded` — a session-level aggregate of outstanding input requests across all chats (`SessionInputRequest` union with `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and diff --git a/docs/specification/root-channel.md b/docs/specification/root-channel.md index 7cdcbee2..312f3423 100644 --- a/docs/specification/root-channel.md +++ b/docs/specification/root-channel.md @@ -30,6 +30,18 @@ RootState { The session list is **not** part of root state. Clients fetch it imperatively via [`listSessions`](/reference/root#listsessions) and patch it from `root/*` notifications described below. +### Paginating the catalogue + +A large catalogue can be fetched incrementally. [`listSessions`](/reference/root#listsessions) accepts an optional `limit` (the maximum number of entries the client wants in the page — the server SHOULD respect it, but MAY return fewer and MAY impose its own upper cap) and an optional opaque `cursor`. The result carries the page in `items` plus an optional `nextCursor`: + +- To fetch the first page, omit `cursor`. Supply `limit` to bound the page size. +- If the result includes a `nextCursor`, more entries exist — pass it back as `cursor` on the next call to fetch the following page. +- A missing `nextCursor` signals the end of the catalogue. + +The cursor is **opaque and server-defined**: the server picks the ordering and keyset, exactly as `fetchTurns` exposes `before`/`hasMore` without dictating storage. Clients MUST NOT parse, modify, or persist a cursor across connections. An unrecognised cursor SHOULD be rejected with an `InvalidParams` error. The server SHOULD return most-recently-modified entries first, so the first page is the immediately useful one. + +Pagination is fully additive. A client that omits `limit`/`cursor` and ignores `nextCursor` sees the pre-pagination behaviour (subject to any server-imposed cap), and a server that does not paginate ignores the inputs and returns everything in one page. Pagination governs only the initial and backfill fetches — the `root/session*` notifications keep an already-loaded page live exactly as before. + ## Methods and events on this channel This section lists wire methods that are interpreted in the context of diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 2f09e098..b3778841 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -18,6 +18,30 @@ "channel" ] }, + "PaginatedParams": { + "type": "object", + "description": "Cursor-based pagination inputs, mixed into the params of any list command\nthat can page a large result set (e.g. {@link ListSessionsParams |\n`listSessions`}). The paired output is {@link PaginatedResult}.\n\nPagination is **opaque and cursor-based**, mirroring the shape `fetchTurns`\nalready uses for chat history: the server owns the ordering and keyset, and\nthe client walks pages by echoing the cursor from the previous\n{@link PaginatedResult.nextCursor} back on the next request.\n\nThe contract every paginated command shares:\n\n- To fetch the first page, omit `cursor`. Supply `limit` to bound the page.\n- If the result carries a {@link PaginatedResult.nextCursor}, more entries\n exist — pass it back as `cursor` to fetch the following page. A missing\n `nextCursor` signals the end of the collection.\n- Cursors are **server-defined and opaque**: clients MUST NOT parse, modify,\n or persist them across connections. An unrecognised cursor SHOULD be\n rejected with an `InvalidParams` error.\n- Pagination is **fully additive**: a client that omits `limit`/`cursor` and\n ignores `nextCursor` sees the pre-pagination behaviour (subject to any\n server-imposed cap), and a server that does not paginate ignores the inputs\n and returns everything in a single page.", + "properties": { + "limit": { + "type": "number", + "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." + }, + "cursor": { + "type": "string", + "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + } + } + }, + "PaginatedResult": { + "type": "object", + "description": "Cursor-based pagination output, extended by the result of any list command\nthat can page a large result set (e.g. {@link ListSessionsResult |\n`listSessions`}). See {@link PaginatedParams} for the full pagination\ncontract shared by every paginated command.", + "properties": { + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." + } + } + }, "InitializeParams": { "type": "object", "description": "Establishes a new connection and negotiates the protocol version.\nThis MUST be the first message sent by the client.", @@ -692,7 +716,7 @@ }, "ListSessionsParams": { "type": "object", - "description": "Returns a list of session summaries. Used to populate session lists and sidebars.\n\nThe session list is **not** part of the state tree because it can be arbitrarily\nlarge. Clients fetch it imperatively and maintain a local cache updated by\n`root/sessionAdded` and `root/sessionRemoved` notifications.", + "description": "Returns a list of session summaries. Used to populate session lists and sidebars.\n\nThe session list is **not** part of the state tree because it can be arbitrarily\nlarge. Clients fetch it imperatively and maintain a local cache updated by\n`root/sessionAdded` and `root/sessionRemoved` notifications.\n\nA large catalogue can be fetched incrementally via the {@link PaginatedParams}\n`limit`/`cursor` inputs (see that type for the full pagination contract). The\nserver SHOULD return most-recently-modified entries first, so the first page\nis the immediately useful one. The `root/session*` notifications keep an\nalready-fetched page live; pagination governs only the initial and backfill\nfetches.", "properties": { "channel": { "type": "string", @@ -700,6 +724,14 @@ "ahp-root://" ] }, + "limit": { + "type": "number", + "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." + }, + "cursor": { + "type": "string", + "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + }, "filter": { "type": "object", "description": "Optional filter criteria" @@ -713,12 +745,16 @@ "type": "object", "description": "Result of the `listSessions` command.", "properties": { + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." + }, "items": { "type": "array", "items": { "$ref": "#/$defs/SessionSummary" }, - "description": "The list of session summaries." + "description": "The list of session summaries. The server SHOULD order them\nmost-recently-modified first." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 0799b553..d644cbb5 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -4425,6 +4425,30 @@ "channel" ] }, + "PaginatedParams": { + "type": "object", + "description": "Cursor-based pagination inputs, mixed into the params of any list command\nthat can page a large result set (e.g. {@link ListSessionsParams |\n`listSessions`}). The paired output is {@link PaginatedResult}.\n\nPagination is **opaque and cursor-based**, mirroring the shape `fetchTurns`\nalready uses for chat history: the server owns the ordering and keyset, and\nthe client walks pages by echoing the cursor from the previous\n{@link PaginatedResult.nextCursor} back on the next request.\n\nThe contract every paginated command shares:\n\n- To fetch the first page, omit `cursor`. Supply `limit` to bound the page.\n- If the result carries a {@link PaginatedResult.nextCursor}, more entries\n exist — pass it back as `cursor` to fetch the following page. A missing\n `nextCursor` signals the end of the collection.\n- Cursors are **server-defined and opaque**: clients MUST NOT parse, modify,\n or persist them across connections. An unrecognised cursor SHOULD be\n rejected with an `InvalidParams` error.\n- Pagination is **fully additive**: a client that omits `limit`/`cursor` and\n ignores `nextCursor` sees the pre-pagination behaviour (subject to any\n server-imposed cap), and a server that does not paginate ignores the inputs\n and returns everything in a single page.", + "properties": { + "limit": { + "type": "number", + "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." + }, + "cursor": { + "type": "string", + "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + } + } + }, + "PaginatedResult": { + "type": "object", + "description": "Cursor-based pagination output, extended by the result of any list command\nthat can page a large result set (e.g. {@link ListSessionsResult |\n`listSessions`}). See {@link PaginatedParams} for the full pagination\ncontract shared by every paginated command.", + "properties": { + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." + } + } + }, "InitializeParams": { "type": "object", "description": "Establishes a new connection and negotiates the protocol version.\nThis MUST be the first message sent by the client.", @@ -5099,7 +5123,7 @@ }, "ListSessionsParams": { "type": "object", - "description": "Returns a list of session summaries. Used to populate session lists and sidebars.\n\nThe session list is **not** part of the state tree because it can be arbitrarily\nlarge. Clients fetch it imperatively and maintain a local cache updated by\n`root/sessionAdded` and `root/sessionRemoved` notifications.", + "description": "Returns a list of session summaries. Used to populate session lists and sidebars.\n\nThe session list is **not** part of the state tree because it can be arbitrarily\nlarge. Clients fetch it imperatively and maintain a local cache updated by\n`root/sessionAdded` and `root/sessionRemoved` notifications.\n\nA large catalogue can be fetched incrementally via the {@link PaginatedParams}\n`limit`/`cursor` inputs (see that type for the full pagination contract). The\nserver SHOULD return most-recently-modified entries first, so the first page\nis the immediately useful one. The `root/session*` notifications keep an\nalready-fetched page live; pagination governs only the initial and backfill\nfetches.", "properties": { "channel": { "type": "string", @@ -5107,6 +5131,14 @@ "ahp-root://" ] }, + "limit": { + "type": "number", + "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." + }, + "cursor": { + "type": "string", + "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + }, "filter": { "type": "object", "description": "Optional filter criteria" @@ -5120,12 +5152,16 @@ "type": "object", "description": "Result of the `listSessions` command.", "properties": { + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." + }, "items": { "type": "array", "items": { "$ref": "#/$defs/SessionSummary" }, - "description": "The list of session summaries." + "description": "The list of session summaries. The server SHOULD order them\nmost-recently-modified first." } }, "required": [ diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index fb139629..e3e9627e 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1890,6 +1890,8 @@ function checkExhaustiveness(project: Project): void { 'URI', 'JsonPrimitive', 'BaseParams', + 'PaginatedParams', + 'PaginatedResult', 'StringOrMarkdown', 'ToolCallState', 'StateAction', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index a02f8aa1..01a9fe46 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1871,6 +1871,8 @@ function checkExhaustiveness(project: Project): void { 'URI', // type alias for string 'JsonPrimitive', // primitive JSON value alias; mapped to JsonElement 'BaseParams', // marker base interface; flattened into each command params struct + 'PaginatedParams', // base interface; flattened into each paginated command params struct + 'PaginatedResult', // base interface; flattened into each paginated command result struct // PingParams shape is `interface PingParams extends BaseParams { channel: 'ahp-root://' }` // (i.e. a `BaseParams` with `channel` narrowed to a string literal). We don't // emit a dedicated data class because the only useful payload is the diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 8021605e..3d408b9a 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1787,6 +1787,8 @@ function checkExhaustiveness(project: Project): void { 'URI', 'JsonPrimitive', 'BaseParams', + 'PaginatedParams', // base interface; flattened into each paginated command params struct + 'PaginatedResult', // base interface; flattened into each paginated command result struct 'StringOrMarkdown', 'ToolCallState', 'StateAction', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 5bd803a1..d32c7630 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1898,6 +1898,8 @@ function checkExhaustiveness(project: Project): void { 'URI', // type alias for string 'JsonPrimitive', // primitive JSON value alias; mapped to AnyCodable 'BaseParams', // marker base interface; flattened into each command params struct + 'PaginatedParams', // base interface; flattened into each paginated command params struct + 'PaginatedResult', // base interface; flattened into each paginated command result struct 'StringOrMarkdown', // generateStringOrMarkdown() 'ToolCallState', // TOOL_CALL_STATE_UNION discriminated union 'StateAction', // StateAction enum in generateActionsFile() diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts index cad9fe7a..e24a39ca 100644 --- a/types/channels-root/commands.ts +++ b/types/channels-root/commands.ts @@ -7,7 +7,7 @@ */ import type { URI } from '../common/state.js'; -import type { BaseParams } from '../common/commands.js'; +import type { BaseParams, PaginatedParams, PaginatedResult } from '../common/commands.js'; import type { SessionSummary, SessionConfigSchema } from '../channels-session/state.js'; // Re-export schema types so the legacy `commands.ts` aggregator continues to @@ -24,21 +24,47 @@ export type { SessionConfigPropertySchema, SessionConfigSchema } from '../channe * large. Clients fetch it imperatively and maintain a local cache updated by * `root/sessionAdded` and `root/sessionRemoved` notifications. * + * A large catalogue can be fetched incrementally via the {@link PaginatedParams} + * `limit`/`cursor` inputs (see that type for the full pagination contract). The + * server SHOULD return most-recently-modified entries first, so the first page + * is the immediately useful one. The `root/session*` notifications keep an + * already-fetched page live; pagination governs only the initial and backfill + * fetches. + * * @category Commands * @method listSessions * @direction Client → Server * @messageType Request * @version 1 + * @example + * ```jsonc + * // Client → Server (fetch the first page of up to 50 sessions) + * { "jsonrpc": "2.0", "id": 4, "method": "listSessions", + * "params": { "channel": "ahp-root://", "limit": 50 } } + * + * // Server → Client (a cursor signals more entries exist) + * { "jsonrpc": "2.0", "id": 4, "result": { + * "items": [ { "id": "s1", ... }, { "id": "s2", ... } ], + * "nextCursor": "eyJvIjo1MH0=" + * }} + * + * // Client → Server (fetch the next page) + * { "jsonrpc": "2.0", "id": 5, "method": "listSessions", + * "params": { "channel": "ahp-root://", "limit": 50, "cursor": "eyJvIjo1MH0=" } } + * ``` */ -export interface ListSessionsParams extends BaseParams { +export interface ListSessionsParams extends BaseParams, PaginatedParams { channel: 'ahp-root://'; /** Optional filter criteria */ filter?: object; } /** Result of the `listSessions` command. */ -export interface ListSessionsResult { - /** The list of session summaries. */ +export interface ListSessionsResult extends PaginatedResult { + /** + * The list of session summaries. The server SHOULD order them + * most-recently-modified first. + */ items: SessionSummary[]; } diff --git a/types/common/commands.ts b/types/common/commands.ts index b02a8716..c674d52e 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -37,6 +37,67 @@ export interface BaseParams { channel: URI; } +// ─── Pagination ────────────────────────────────────────────────────────────── + +/** + * Cursor-based pagination inputs, mixed into the params of any list command + * that can page a large result set (e.g. {@link ListSessionsParams | + * `listSessions`}). The paired output is {@link PaginatedResult}. + * + * Pagination is **opaque and cursor-based**, mirroring the shape `fetchTurns` + * already uses for chat history: the server owns the ordering and keyset, and + * the client walks pages by echoing the cursor from the previous + * {@link PaginatedResult.nextCursor} back on the next request. + * + * The contract every paginated command shares: + * + * - To fetch the first page, omit `cursor`. Supply `limit` to bound the page. + * - If the result carries a {@link PaginatedResult.nextCursor}, more entries + * exist — pass it back as `cursor` to fetch the following page. A missing + * `nextCursor` signals the end of the collection. + * - Cursors are **server-defined and opaque**: clients MUST NOT parse, modify, + * or persist them across connections. An unrecognised cursor SHOULD be + * rejected with an `InvalidParams` error. + * - Pagination is **fully additive**: a client that omits `limit`/`cursor` and + * ignores `nextCursor` sees the pre-pagination behaviour (subject to any + * server-imposed cap), and a server that does not paginate ignores the inputs + * and returns everything in a single page. + * + * @category Commands + */ +export interface PaginatedParams { + /** + * Maximum number of entries to return in this page. The server SHOULD respect + * this bound but MAY return fewer entries and MAY impose its own upper cap. + * Omit to let the server choose the page size. + */ + limit?: number; + /** + * Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + * Omit to fetch the first page. Cursors are server-defined and MUST be treated + * as opaque — do not parse, modify, or persist them across connections. An + * unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + */ + cursor?: string; +} + +/** + * Cursor-based pagination output, extended by the result of any list command + * that can page a large result set (e.g. {@link ListSessionsResult | + * `listSessions`}). See {@link PaginatedParams} for the full pagination + * contract shared by every paginated command. + * + * @category Commands + */ +export interface PaginatedResult { + /** + * Opaque cursor for the next page. Present when more entries exist beyond the + * returned page; absent signals the end of the collection. Pass it back as + * {@link PaginatedParams.cursor} to fetch the following page. + */ + nextCursor?: string; +} + // ─── initialize ────────────────────────────────────────────────────────────── /** diff --git a/types/index.ts b/types/index.ts index bcb11e73..ccec8598 100644 --- a/types/index.ts +++ b/types/index.ts @@ -275,6 +275,8 @@ export type { DisposeChatParams, CreateTerminalParams, DisposeTerminalParams, + PaginatedParams, + PaginatedResult, ListSessionsParams, ListSessionsResult, ResourceReadParams, From 553016634c9d6612108e22cd2cb5d699a1778318 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 1 Jul 2026 15:05:56 -0700 Subject: [PATCH 2/2] Remove undefined filter field from ListSessionsParams The `filter` field was an untyped `object` placeholder with no defined semantics. Drop it rather than ship an unspecified field; it will be reintroduced with a concrete shape once session filtering/sorting is specified. Updates the two host-runtime call sites (Rust, TypeScript) that named the field, regenerates all clients + schema, and records the removal in every CHANGELOG. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ clients/go/CHANGELOG.md | 6 ++++++ clients/go/ahptypes/commands.generated.go | 2 -- clients/kotlin/CHANGELOG.md | 6 ++++++ .../agenthostprotocol/generated/Commands.generated.kt | 6 +----- clients/rust/CHANGELOG.md | 6 ++++++ clients/rust/crates/ahp-types/src/commands.rs | 3 --- clients/rust/crates/ahp/src/hosts/runtime.rs | 1 - .../AgentHostProtocol/Generated/Commands.generated.swift | 6 +----- clients/swift/CHANGELOG.md | 6 ++++++ clients/typescript/CHANGELOG.md | 6 ++++++ clients/typescript/src/client/hosts/runtime.ts | 1 - schema/commands.schema.json | 4 ---- schema/errors.schema.json | 4 ---- types/channels-root/commands.ts | 2 -- 15 files changed, 38 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d54a5d44..8ee62817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,12 @@ changes accumulate. Track in-flight protocol changes via PRs touching incrementally. Fully additive — omitting the fields preserves today's behaviour. +### Removed + +- `filter` field from `ListSessionsParams`. It was an untyped `object` + placeholder with no defined semantics; it will be reintroduced with a concrete + shape once session filtering/sorting is specified. + ## [0.5.1] — Unreleased Spec version: `0.5.1` diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index 2b3bf92a..fd43e073 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -41,6 +41,12 @@ 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. +### Removed + +- `Filter` field from `ListSessionsParams`. It was an untyped placeholder with + no defined semantics; it will return with a concrete shape once session + filtering/sorting is specified. + ### Fixed - `SnapshotState.UnmarshalJSON` now decodes the `Chat` variant. Variant diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 287c0ccb..c7b03f49 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -332,8 +332,6 @@ type ListSessionsParams struct { // as opaque — do not parse, modify, or persist them across connections. An // unrecognised cursor SHOULD be rejected with an `InvalidParams` error. Cursor *string `json:"cursor,omitempty"` - // Optional filter criteria - Filter *json.RawMessage `json:"filter,omitempty"` } // Result of the `listSessions` command. diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index c675e460..eb5c051e 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -40,6 +40,12 @@ 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. +### Removed + +- `filter` field from `ListSessionsParams`. It was an untyped placeholder with + no defined semantics; it will return with a concrete shape once session + filtering/sorting is specified. + ### Fixed - `SnapshotState` now decodes the `Chat` variant. Its serializer previously never 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 80666736..26402250 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 @@ -410,11 +410,7 @@ data class ListSessionsParams( * as opaque — do not parse, modify, or persist them across connections. An * unrecognised cursor SHOULD be rejected with an `InvalidParams` error. */ - val cursor: String? = null, - /** - * Optional filter criteria - */ - val filter: JsonElement? = null + val cursor: String? = null ) @Serializable diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 8c6ae18f..68deb44e 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -47,6 +47,12 @@ matching `## [X.Y.Z]` heading is missing from this file. `delivery: None`; use `SubscribeParams::new(channel)` or `Client::subscribe` to keep the default delivery behavior. +### Removed + +- `filter` field from `ListSessionsParams`. It was an untyped placeholder with + no defined semantics; it will return with a concrete shape once session + filtering/sorting is specified. + ## [0.5.0] — 2026-06-26 Implements AHP 0.5.0. diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index a8c34e8c..b55dd88f 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -415,9 +415,6 @@ pub struct ListSessionsParams { /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, - /// Optional filter criteria - #[serde(default, skip_serializing_if = "Option::is_none")] - pub filter: Option, } /// Result of the `listSessions` command. diff --git a/clients/rust/crates/ahp/src/hosts/runtime.rs b/clients/rust/crates/ahp/src/hosts/runtime.rs index 795a1a2c..51a5c1d1 100644 --- a/clients/rust/crates/ahp/src/hosts/runtime.rs +++ b/clients/rust/crates/ahp/src/hosts/runtime.rs @@ -317,7 +317,6 @@ impl HostRuntime { "listSessions", ListSessionsParams { channel: ROOT_RESOURCE_URI.to_string(), - filter: None, limit: None, cursor: None, }, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 6c646b8f..6113d421 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -404,19 +404,15 @@ public struct ListSessionsParams: Codable, Sendable { /// as opaque — do not parse, modify, or persist them across connections. An /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. public var cursor: String? - /// Optional filter criteria - public var filter: AnyCodable? public init( channel: String, limit: Int? = nil, - cursor: String? = nil, - filter: AnyCodable? = nil + cursor: String? = nil ) { self.channel = channel self.limit = limit self.cursor = cursor - self.filter = filter } } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index f4f02d3e..7bf7e226 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -43,6 +43,12 @@ the tag matches the version pinned in [`VERSION`](VERSION). - Optional `model` and `tools` fields on `AgentCustomization` for a custom agent's pinned model and tool allowlist. +### Removed + +- `filter` field from `ListSessionsParams`. It was an untyped placeholder with + no defined semantics; it will return with a concrete shape once session + filtering/sorting is specified. + ### Fixed - `SnapshotState` now decodes the `chat` variant. Its decoder previously never diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index e171dd78..88476600 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -47,6 +47,12 @@ hotfix escape hatch. - Optional `model` and `tools` fields on `AgentCustomization` for a custom agent's pinned model and tool allowlist. +### Removed + +- `filter` field from `ListSessionsParams`. It was an untyped placeholder with + no defined semantics; it will return with a concrete shape once session + filtering/sorting is specified. + ## [0.5.0] — 2026-06-26 Implements AHP 0.5.0. diff --git a/clients/typescript/src/client/hosts/runtime.ts b/clients/typescript/src/client/hosts/runtime.ts index 0e3f9be1..f5e2c015 100644 --- a/clients/typescript/src/client/hosts/runtime.ts +++ b/clients/typescript/src/client/hosts/runtime.ts @@ -629,7 +629,6 @@ export class HostRuntime { const res = await raceWithAbort( client.request('listSessions', { channel: ROOT_RESOURCE_URI as 'ahp-root://', - filter: undefined, }), cancelSignal, ); diff --git a/schema/commands.schema.json b/schema/commands.schema.json index b3778841..0136d547 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -731,10 +731,6 @@ "cursor": { "type": "string", "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." - }, - "filter": { - "type": "object", - "description": "Optional filter criteria" } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index d644cbb5..08d3863e 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -5138,10 +5138,6 @@ "cursor": { "type": "string", "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." - }, - "filter": { - "type": "object", - "description": "Optional filter criteria" } }, "required": [ diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts index e24a39ca..ff3c706b 100644 --- a/types/channels-root/commands.ts +++ b/types/channels-root/commands.ts @@ -55,8 +55,6 @@ export type { SessionConfigPropertySchema, SessionConfigSchema } from '../channe */ export interface ListSessionsParams extends BaseParams, PaginatedParams { channel: 'ahp-root://'; - /** Optional filter criteria */ - filter?: object; } /** Result of the `listSessions` command. */