diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc897eb..8ee62817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,18 @@ 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. + +### 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 diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index b1a4b216..fd43e073 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 @@ -36,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 70be4320..c7b03f49 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -313,16 +313,35 @@ 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"` - // Optional filter criteria - Filter *json.RawMessage `json:"filter,omitempty"` + // 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"` } // 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..eb5c051e 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 @@ -35,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 75d26873..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 @@ -399,15 +399,31 @@ data class ListSessionsParams( */ val channel: String, /** - * Optional filter criteria + * 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 filter: JsonElement? = null + 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 ) @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..68deb44e 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 @@ -42,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 7ce8f58a..b55dd88f 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -392,21 +392,42 @@ 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, - /// Optional filter criteria + /// 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 filter: Option, + pub cursor: Option, } /// Result of the `listSessions` command. #[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..51a5c1d1 100644 --- a/clients/rust/crates/ahp/src/hosts/runtime.rs +++ b/clients/rust/crates/ahp/src/hosts/runtime.rs @@ -317,7 +317,8 @@ impl HostRuntime { "listSessions", 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..6113d421 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -395,25 +395,41 @@ public struct DisposeChatParams: Codable, Sendable { public struct ListSessionsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String - /// Optional filter criteria - public var filter: AnyCodable? + /// 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? public init( channel: String, - filter: AnyCodable? = nil + limit: Int? = nil, + cursor: String? = nil ) { self.channel = channel - self.filter = filter + self.limit = limit + self.cursor = cursor } } 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..7bf7e226 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 @@ -38,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 1dccdc9a..88476600 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 @@ -42,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/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..0136d547 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,9 +724,13 @@ "ahp-root://" ] }, - "filter": { - "type": "object", - "description": "Optional filter criteria" + "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." } }, "required": [ @@ -713,12 +741,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..08d3863e 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,9 +5131,13 @@ "ahp-root://" ] }, - "filter": { - "type": "object", - "description": "Optional filter criteria" + "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." } }, "required": [ @@ -5120,12 +5148,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..ff3c706b 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,45 @@ 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,