From 80c1736e0c8eaf6ff61ab4f8bfa13462ca7f2dee Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Sat, 12 Sep 2026 00:14:09 +0300 Subject: [PATCH 1/3] feat: bind cursor lists by inclusion and return page sequences A query that carries the admitted limit, order, and cursor parameters now derives from ListRequest whatever else it declares, so a filter beside the spine no longer pushes a list out of cursor pagination: the session list gains EnumerateSessionsAsync, and a continuation is the first request again with order dropped and the returned cursor in place, carrying every filter unchanged. A spine-carrying query that declares a required parameter refuses pagination, because a continuation must be constructible from no request at all. Enumerate*Async returns CursorSequence: still an IAsyncEnumerable, and its Pages property replays the same traversal as the generated page envelopes, so page metadata no longer requires paging by hand. Both doors drive the one traversal core; each enumeration issues its own requests. Increment benchmark on the pagination ladder: 394,048 to 394,544 allocated bytes over a three-page, thirty-item walk; the manual-paging control is unchanged. --- CHANGELOG.md | 28 ++- CONTEXT.md | 5 + README.md | 2 +- ...-cursor-pagination-keeps-explicit-pages.md | 34 +-- docs/architecture/client-runtime.md | 25 ++- docs/architecture/protocol-and-generation.md | 11 +- docs/guide/pagination.md | 108 ++++++--- .../Internal/Pagination/CursorPaginator.cs | 21 +- .../MessageListResponseAdapter.cs | 6 +- .../SessionListResponseAdapter.cs | 35 ++- src/OpenCode.Sdk/Pagination/CursorSequence.cs | 71 ++++++ src/OpenCode.Sdk/Sessions/SessionClient.cs | 6 +- .../Sessions/SessionListRequest.cs | 16 +- src/OpenCode.Sdk/Sessions/SessionsClient.cs | 14 ++ .../Benchmarks/PaginationBenchmarks.cs | 17 +- .../Sessions/SessionClientPaginationTests.cs | 206 +++++++++++++++++- .../Sessions/SessionsClientContractTests.cs | 104 +++++++++ .../Sessions/SessionsClientLiveTests.cs | 68 ++++++ .../Snapshots/PublicApi.verified.txt | 14 +- .../Binding/OperationPlanBinderTests.cs | 111 +++++++++- ...ce_Virtual_Delegating_Clients.verified.txt | 6 +- ...roduce_Status_Mapped_Adapters.verified.txt | 6 +- .../Support/EmitterPlanFixture.cs | 1 + .../Binding/ListRequestSpinePolicy.cs | 39 ++++ .../Binding/Models/PaginationPlan.cs | 3 + .../Binding/PaginationFacetBinder.cs | 10 + .../Binding/QueryRequestFacetBinder.cs | 25 +-- .../Generator/Binding/ReservedNamePolicy.cs | 1 + .../Emission/PaginationMethodEmitter.cs | 9 +- .../Emission/ResponseAdapterEmitter.cs | 41 ++-- 30 files changed, 890 insertions(+), 153 deletions(-) create mode 100644 src/OpenCode.Sdk/Pagination/CursorSequence.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/ListRequestSpinePolicy.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c10e098..5992b240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,36 @@ Each released version links straight to its GitHub Release tag. ## [Unreleased] -Nothing yet. Nightly builds of `master` are on +Nightly builds of `master` are on [GitHub Packages](README.md#nightly-builds-github-packages) as `0.8.0-nightly.{yyyyMMdd}.{shortSha}`. +### ✨ New features + +- **Session listing enumerates too.** `SessionsClient.EnumerateSessionsAsync` joins + `SessionClient.EnumerateMessagesAsync` as an automatic cursor walk. A list query now binds the + shared `ListRequest` spine whenever it *includes* `limit`, `order`, and `cursor` — extra filters + no longer disqualify it — so `SessionListRequest` derives from `ListRequest` and gains the + companion. Every filter of the first request rides each continuation unchanged; only the + first-page-only `order` is dropped and the opaque cursor replaced. +- **Every automatic walk can be read as pages.** `Enumerate*Async` now returns + `CursorSequence`, whose `Pages` property yields each generated response envelope — + `Status`, `Cursor`, `RawBody` and all — instead of only the items. There is no new page type and + no page-size knob: a page is the response the server returned for one request. The item door and + the page door are independent walks of the same recipe, so enumerating both sends both sets of + requests. + +### 🔧 Changes + +- `SessionClient.EnumerateMessagesAsync` returns `CursorSequence` instead of `IAsyncEnumerable`. `await foreach` over it + is unchanged, and code that stored the result in a variable typed `IAsyncEnumerable` still + compiles, because `CursorSequence` implements `IAsyncEnumerable`. Code that + declared the variable with `var` and then assigned an `IAsyncEnumerable` to the same variable + is the one shape that needs its type written out. +- `SessionListRequest` no longer declares `Limit`, `Order`, and `Cursor` itself; it inherits all + three from `ListRequest`. Reading and initializing them is unchanged. + ## [0.8.0-preview.1] - 2026-09-09 The first published release. Everything below describes the surface as it ships; there is no diff --git a/CONTEXT.md b/CONTEXT.md index 4dc786b8..f13eefc4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -258,6 +258,11 @@ property is optional has no payload the envelope can promise and refuses at bind A sub-client bound to one resource id (e.g. a session) — partial application over the shared pipeline; never caches server state. +**Cursor sequence** (`CursorSequence`): +The value a generated `Enumerate*Async` companion returns. It enumerates as the items of every page +in order and exposes the same traversal's page envelopes through `Pages`. It is a recipe rather than +a buffer: each enumeration issues its own requests. + **PTY session** (`PtySession`): The working object over one PTY connection: read frames, write input, dispose to close. One of the two doors that build their own transport instead of riding the HTTP pipeline (the persistent PTY diff --git a/README.md b/README.md index 7dd4942e..3067c38e 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ hand-written `PtySession` and `PersistentPtySession` doors described in | [Streaming](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/streaming.md) | The global event bus and per-session server-sent event streams | | [Terminals](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/terminals.md) | PTY and persistent-PTY sessions over the WebSocket doors | | [Errors and responses](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/errors-and-responses.md) | Throwing versus `NoThrow`, and the typed error model | -| [Pagination](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/pagination.md) | Cursor-carrying list envelopes and `EnumerateMessagesAsync` | +| [Pagination](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/pagination.md) | Cursor-carrying list envelopes, `Enumerate*Async`, and its `Pages` | Architecture, decision records, and engineering policy live under [`docs/`](https://github.com/Blind-Striker/opencode-sdk-dotnet/tree/master/docs) — start at [`AGENTS.md`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/AGENTS.md) if you want the internals rather than the API. diff --git a/docs/adr/0017-cursor-pagination-keeps-explicit-pages.md b/docs/adr/0017-cursor-pagination-keeps-explicit-pages.md index 3eac34b1..5e2572b3 100644 --- a/docs/adr/0017-cursor-pagination-keeps-explicit-pages.md +++ b/docs/adr/0017-cursor-pagination-keeps-explicit-pages.md @@ -1,26 +1,32 @@ # Cursor pagination keeps explicit pages and adds item enumeration -Date: 2026-08-20 +Date: 2026-09-12 An operation whose pinned request and response bind to the supported cursor-list dialect keeps its -generated one-page `List*Async` method and additively emits `Enumerate*Async` as a lazy -`IAsyncEnumerable`. The page method remains the cursor/metadata and `NoThrow` path; automatic -item traversal always throws API errors when their page is reached. This was chosen over a local -`AsyncPageable`/`Page` family because the generated response envelope already is the page, -including the bidirectional `ListCursor`, while an Azure-shaped wrapper would duplicate that public -vocabulary and invite an integer page-size contract absent from the pin. +generated one-page `List*Async` method and additively emits `Enumerate*Async`, which returns a +`CursorSequence`: a lazy `IAsyncEnumerable` whose `Pages` property replays the +same traversal as page envelopes. The page method remains the `NoThrow` and `cursor.previous` path; +automatic traversal takes no per-call options and always throws API errors when their page is +reached. A local `AsyncPageable`/`Page` family was rejected and stays rejected: `TPage` is the +generated response envelope itself, including the bidirectional `ListCursor`, so the page door +introduces no second public page vocabulary and no integer page-size contract absent from the pin. -The first request is sent unchanged. Continuations retain the pinned string `limit`, omit -first-page-only `order`, and pass the opaque returned `cursor.next`; only an absent next cursor ends -the sequence. Generated adapters project each admitted operation onto one hand-written traversal -core. Other pagination dialects require their own mechanically proven binding support rather than -being inferred from descriptions or forced through `ListRequest`. +The first request is sent unchanged. A continuation is that same request with first-page-only +`order` dropped and the opaque returned `cursor.next` put in place, so the pinned string `limit` and +every filter the query declares travel every page; only an absent next cursor ends the sequence. A +query binds `ListRequest` by inclusion — the three admitted members must be present in their +admitted shapes, filters may ride beside them — and a spine-carrying query that declares a required +parameter is refused, because a continuation must be constructible from no request at all. Generated +adapters project each admitted operation onto one hand-written traversal core. Other pagination +dialects require their own mechanically proven binding support rather than being inferred from +descriptions or forced through `ListRequest`. ## Consequences - The automatic sequence is finite pull-based pagination over ordinary buffered HTTP responses, not a server stream and not a second transport path. - Cancellation is observed during page requests and between already-buffered items. -- `cursor.previous` and page metadata remain available through explicit page calls; a future page - sequence can be added without changing this surface if a concrete consumer earns it. +- `cursor.previous` and per-call `NoThrow` remain reasons to call the page method directly; page + metadata is otherwise reachable from the automatic traversal through `Pages`, which yields the + same envelopes the page method returns. - Cursors are never decoded, normalized, incremented, compared, or cycle-checked (ADR-0013). diff --git a/docs/architecture/client-runtime.md b/docs/architecture/client-runtime.md index 0824de1d..2e867c23 100644 --- a/docs/architecture/client-runtime.md +++ b/docs/architecture/client-runtime.md @@ -1,6 +1,6 @@ # Client Runtime Architecture -Date: 2026-09-08 +Date: 2026-09-12 Canonical current rules for client construction, transport ownership, API errors, streams, and the local server launcher. Protocol and generated-model rules live in @@ -319,14 +319,21 @@ local server launcher. Protocol and generated-model rules live in ## Pagination - A supported cursor-list operation has two generated doors: `List*Async` returns one endpoint- - specific page envelope, while `Enumerate*Async` lazily yields its items across pages. Explicit - pages retain cursor/status metadata and per-call `NoThrow`; automatic item traversal has no - response envelope and therefore always throws API errors when their page is reached (ADR-0007, - ADR-0017). -- `ListRequest` carries the pinned string `limit`, first-page `order`, and opaque `cursor` channels; - `ListCursor` preserves the response's optional `previous` and `next` values. The first automatic - request is sent unchanged, including an order-plus-cursor pair. Each continuation retains the - initial `limit`, omits `order`, and sends the returned `cursor.next` without decoding it. + specific page envelope, while `Enumerate*Async` returns a `CursorSequence` that + lazily yields the items across pages and, through its `Pages` property, the same traversal's page + envelopes. Explicit pages retain per-call `NoThrow`; automatic traversal never takes per-call + options and therefore always throws API errors when their page is reached (ADR-0007, ADR-0017). +- `CursorSequence` is a recipe, not a buffer: the item door and the page door each + drive the one traversal core independently, so enumerating either performs its own requests, and + enumerating both performs both walks. Its `TPage` is the generated response type — no page + vocabulary and no page-size contract is introduced. The token given to `Enumerate*Async` and the + token given to `GetAsyncEnumerator`/`WithCancellation` are both observed. +- `ListRequest` carries the pinned string `limit`, first-page `order`, and opaque `cursor` channels, + and a derived request may declare its own filters beside them; `ListCursor` preserves the + response's optional `previous` and `next` values. The first automatic request is sent unchanged, + including an order-plus-cursor pair. Each continuation is that request again with `order` dropped + and the returned `cursor.next` put in place, so `limit` and every filter travel every page and a + filter added to the pinned query needs no generator change. - A missing `next` cursor is the only normal end signal. An empty page with `next` continues; `previous` remains available through explicit page calls. Cursor values are never normalized, incremented, compared, or cycle-checked. Cancellation reaches each request and is checked between diff --git a/docs/architecture/protocol-and-generation.md b/docs/architecture/protocol-and-generation.md index 91c1cb51..14e8ff3c 100644 --- a/docs/architecture/protocol-and-generation.md +++ b/docs/architecture/protocol-and-generation.md @@ -1,6 +1,6 @@ # Protocol and Generation Architecture -Date: 2026-09-03 +Date: 2026-09-12 Canonical current rules for the protocol surface, generator, generated models, and runtime materialization boundary. ADRs record why these decisions were made; dated research records the @@ -191,9 +191,14 @@ dispatch instead of routing it through ADR-0009's unknown carrier (ADR-0015). the declared-header channel into the pipeline. The header value stays a caller concern and never enters curation or generated code, and a header-bearing operation forgoes the cursor-enumeration companion (ADR-0013, ADR-0021). +- A query binds the `ListRequest` base by inclusion: it must carry the admitted optional `limit`, + `order`, and `cursor` parameters in their admitted shapes, and every further optional parameter + binds beside them as its own request property. A missing spine member, or one whose schema is not + the admitted shape, keeps the request flat and unpaginated. - A cursor-list operation emits an additive `Enumerate*Async` companion only when binding proves the - admitted `ListRequest` query, `ListCursor` response, item collection, and operation signature. - Generated response adapters project page items, the opaque next cursor, and the continuation + admitted `ListRequest` query, `ListCursor` response, item collection, and operation signature, and + the query declares no required parameter — a continuation must be constructible from no request at + all. Generated response adapters project page items, the opaque next cursor, and the continuation request into one hand-written traversal core. Method names, descriptions, and upstream source do not independently confer pagination semantics (ADR-0013, ADR-0017). - A query parameter binds by shape: a required parameter becomes a `required`, non-nullable diff --git a/docs/guide/pagination.md b/docs/guide/pagination.md index ad5ff435..03e793bb 100644 --- a/docs/guide/pagination.md +++ b/docs/guide/pagination.md @@ -1,15 +1,16 @@ # 📑 Pagination -Date: 2026-08-31 +Date: 2026-09-11 Two operations in the API return more than fits in one response, and both use the same opaque -cursor envelope: **listing a session's messages** and **listing sessions**. Everything else returns -its whole answer at once. +cursor envelope: **listing a session's messages** and **listing sessions**. Both can be paged by +hand or walked for you. Everything else returns its whole answer at once. - [🧾 The cursor envelope](#-the-cursor-envelope) - [🎛️ Shaping the request](#️-shaping-the-request) - [🔁 Paging by hand](#-paging-by-hand) - [♾️ Or let the SDK page for you](#️-or-let-the-sdk-page-for-you) +- [📄 The same walk, one page at a time](#-the-same-walk-one-page-at-a-time) - [⚖️ Which one to use](#️-which-one-to-use) ## 🧾 The cursor envelope @@ -53,13 +54,14 @@ Three channels do the paging, and both request types carry all three: | `Order` | `ListOrder?` | `Ascending` or `Descending`. **First page only.** | | `Cursor` | `string?` | The opaque continuation from the previous page's `Cursor.Next`. | -`MessageListRequest` inherits them from the shared abstract `ListRequest`. -`SessionListRequest` declares the same three itself, next to its own filters — `Search`, -`Project`, `Workspace`, `Directory`, `Subpath`, and `ParentId`. +`MessageListRequest` and `SessionListRequest` both inherit all three from the shared abstract +`ListRequest`. A request may also carry filters of its own beside them: `SessionListRequest` adds +`Search`, `Project`, `Workspace`, `Directory`, `Subpath`, and `ParentId`. -> **🧭 `Order` belongs to the first request.** The order is fixed when the sequence starts; a -> continuation carries `Limit` and `Cursor` only. Keeping `Order` on later pages is not something -> the SDK will correct for you — the snippets below drop it deliberately. +> **🧭 `Order` belongs to the first request.** The order is fixed when the walk starts; a +> continuation carries `Limit`, `Cursor`, and every filter — but not `Order`. Paging by hand, that +> is yours to get right, and the snippets below drop it deliberately. Paging automatically, the SDK +> does it for you. ## 🔁 Paging by hand @@ -110,13 +112,14 @@ while (true) } ``` -Manual paging is what you want when you need the page metadata: the status of each request, -`NoThrow` handling per page, or `Cursor.Previous` to walk backwards. +Manual paging is what you want when the walk itself is yours: per-page `NoThrow` handling, +`Cursor.Previous` to move backwards, or a cursor you stop at and resume from later. ## ♾️ Or let the SDK page for you -`SessionClient.EnumerateMessagesAsync` is the automatic companion to `ListMessagesAsync`. It yields -the **items**, lazily, following `Next` for you: +`SessionClient.EnumerateMessagesAsync` and `SessionsClient.EnumerateSessionsAsync` are the automatic +companions to `ListMessagesAsync` and `ListSessionsAsync`. Each yields the **items**, lazily, +following `Next` for you: ```csharp var stream = session.EnumerateMessagesAsync(new MessageListRequest @@ -131,33 +134,74 @@ await foreach (var message in stream.WithCancellation(cancellationToken)) } ``` -What it does with your request is exactly the manual loop above: the first request goes out -unchanged, every continuation keeps your `Limit`, drops `Order`, and sends the returned cursor -verbatim. It stops when a page comes back without a `Next`. +Sessions enumerate the same way, filters and all: -Three things to know: +```csharp +var stream = client.Sessions.EnumerateSessionsAsync(new SessionListRequest +{ + Limit = "25", + Order = ListOrder.Descending, + Search = "build", +}); + +await foreach (var session in stream.WithCancellation(cancellationToken)) +{ + Console.WriteLine($"{session.Id} {session.Title}"); +} +``` + +What it does with your request is the manual loop above, with one thing done better: the first +request goes out unchanged, and **every continuation is your request again** — same `Limit`, same +`Search`, `Project`, `ParentId`, and every other filter — with `Order` dropped and the returned +cursor put in place. It stops when a page comes back without a `Next`. + +Four things to know: - **It is a pull sequence over ordinary HTTP calls**, not a stream. Nothing is held open between pages, and stopping early (a `break`, a `return`, a cancelled token) simply means the next page is never requested. -- **It always throws.** There is no envelope to hand you, so an API error on page seven throws +- **It always throws.** No envelope reaches the item loop, so an API error on page seven throws `OpenCodeApiException` out of the `await foreach` — `NoThrow` is not available here. - **Cancellation reaches every request** and is also checked between buffered items, so a token you - cancel mid-page takes effect immediately rather than after the current page drains. + cancel mid-page takes effect immediately rather than after the current page drains. The token you + hand to `Enumerate*Async` and the one you hand to `WithCancellation` are both observed. +- **Each enumeration is its own walk.** Keeping the returned sequence in a variable and enumerating + it twice sends every request twice: it is a recipe, not a buffer. -`EnumerateMessagesAsync` is the **only** automatic companion in the SDK today. Session listing has -no `EnumerateSessionsAsync`; page it with the loop above. +## 📄 The same walk, one page at a time + +`Enumerate*Async` returns a `CursorSequence`. Enumerating it gives you the items; its +`Pages` property gives you the same walk as **page envelopes** — the very `MessageListResponse` or +`SessionListResponse` the one-page call would have handed you, `Status`, `Cursor`, `RawBody` and +all: + +```csharp +await foreach (var page in session.EnumerateMessagesAsync(request) + .Pages + .WithCancellation(cancellationToken)) +{ + Console.WriteLine($"{page.Messages.Count} messages, next {page.Cursor.Next ?? ""}"); +} +``` + +Reach for it when the cursor bookkeeping should be handled but the page is still part of what you +are doing — reporting progress per request, checkpointing `Cursor.Next`, or reading a page's status. +There is no page-size contract here: a page is exactly what the server returned for one request. + +The two doors are independent walks of the same recipe, so enumerating the items and then the pages +performs both walks — two full sets of requests. And `NoThrow` remains unavailable: an error page +throws when the walk reaches it, exactly as it does for items. ## ⚖️ Which one to use -| | `List*Async` | `EnumerateMessagesAsync` | -|---|---|---| -| You get | one page envelope | the items, across all pages | -| Page metadata (`Status`, `Cursor`) | ✅ yes | ❌ no | -| Per-call `NoThrow` | ✅ yes | ❌ no — always throws | -| Backwards paging via `Previous` | ✅ yes | ❌ no | -| Cursor bookkeeping | yours | handled | - -Reach for `EnumerateMessagesAsync` when you just want the messages. Reach for `ListMessagesAsync` -when the page itself is part of what you are doing — rendering a paged UI, checkpointing a cursor, -or treating a failed page as data. +| | `List*Async` | `Enumerate*Async` | `Enumerate*Async().Pages` | +|---|---|---|---| +| You get | one page envelope | the items, across all pages | every page envelope | +| Page metadata (`Status`, `Cursor`) | ✅ yes | ❌ no | ✅ yes | +| Per-call `NoThrow` | ✅ yes | ❌ no — always throws | ❌ no — always throws | +| Backwards paging via `Previous` | ✅ yes | ❌ no | ❌ no | +| Cursor bookkeeping | yours | handled | handled | + +Reach for `Enumerate*Async` when you just want the messages or the sessions, and for its `Pages` +when you want the walk handled but the envelope kept. Reach for `List*Async` when you own the walk: +a paged UI, `Cursor.Previous`, or treating a failed page as data. diff --git a/src/OpenCode.Sdk/Internal/Pagination/CursorPaginator.cs b/src/OpenCode.Sdk/Internal/Pagination/CursorPaginator.cs index ec2c19bd..f003a799 100644 --- a/src/OpenCode.Sdk/Internal/Pagination/CursorPaginator.cs +++ b/src/OpenCode.Sdk/Internal/Pagination/CursorPaginator.cs @@ -5,8 +5,8 @@ namespace OpenCode.Sdk.Internal.Pagination; /// Lazily traverses generated cursor-list operations through their ordinary one-page methods. internal static class CursorPaginator { - /// Creates a lazy item sequence over the operation's opaque next cursors. - public static IAsyncEnumerable EnumerateAsync( + /// Creates a lazy sequence over the operation's opaque next cursors. + public static CursorSequence EnumerateAsync( Func> fetchPage, TRequest? initialRequest, ICursorPageAdapter adapter, @@ -17,10 +17,17 @@ public static IAsyncEnumerable EnumerateAsync ArgumentNullException.ThrowIfNull(fetchPage); ArgumentNullException.ThrowIfNull(adapter); - return EnumerateCoreAsync(fetchPage, initialRequest, adapter, cancellationToken); + return new CursorSequence( + token => EnumeratePagesCoreAsync(fetchPage, initialRequest, adapter, token), + adapter.GetItems, + cancellationToken); } - private static async IAsyncEnumerable EnumerateCoreAsync( + /// + /// The single traversal: one page per request, the next request built from the initial one and + /// the opaque returned cursor, and an absent next cursor as the only end signal. + /// + private static async IAsyncEnumerable EnumeratePagesCoreAsync( Func> fetchPage, TRequest? initialRequest, ICursorPageAdapter adapter, @@ -32,11 +39,7 @@ private static async IAsyncEnumerable EnumerateCoreAsync GetItems(MessageListResponse response) } /// - /// Creates a continuation request carrying the initial limit and opaque next cursor. + /// Creates a continuation request carrying every initial filter beside the opaque next cursor, with the first-page-only order dropped. /// public MessageListRequest CreateNextRequest(MessageListRequest? initialRequest, string cursor) { ArgumentNullException.ThrowIfNull(cursor); - return new MessageListRequest + return (initialRequest ?? new()) with { - Limit = initialRequest?.Limit, + Order = null, Cursor = cursor }; } diff --git a/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionListResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionListResponseAdapter.cs index 82d6d986..e080728e 100644 --- a/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionListResponseAdapter.cs +++ b/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionListResponseAdapter.cs @@ -1,12 +1,14 @@ // Generated by OpenCode.Sdk.Tools from spec/openapi.json. // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using OpenCode.Sdk.Internal.Pagination; using OpenCode.Sdk.Internal.Serialization; +using OpenCode.Sdk.Models; namespace OpenCode.Sdk.Internal.ResponseAdapters; /// /// Adapts the 'GET /api/session' responses onto 'SessionListResponse'. /// -internal sealed class SessionListResponseAdapter : ResponseAdapter +internal sealed class SessionListResponseAdapter : ResponseAdapter, ICursorPageAdapter { private static readonly string[] Status400Tags = ["InvalidCursorError", "InvalidRequestError"]; private static readonly string[] Status401Tags = ["UnauthorizedError"]; @@ -50,6 +52,37 @@ public override SessionListResponse Adapt(int status, string rawBody) }; } + /// + /// Gets the ordered items carried by one successful page. + /// + public IReadOnlyList GetItems(SessionListResponse response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Sessions; + } + + /// + /// Gets the opaque next cursor carried by one successful page. + /// + public string? GetNextCursor(SessionListResponse response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Cursor.Next; + } + + /// + /// Creates a continuation request carrying every initial filter beside the opaque next cursor, with the first-page-only order dropped. + /// + public SessionListRequest CreateNextRequest(SessionListRequest? initialRequest, string cursor) + { + ArgumentNullException.ThrowIfNull(cursor); + return (initialRequest ?? new()) with + { + Order = null, + Cursor = cursor + }; + } + private static SessionListResponse CreateSuccess(int status, SessionListResponseEnvelope envelope) => new() { Status = status, diff --git a/src/OpenCode.Sdk/Pagination/CursorSequence.cs b/src/OpenCode.Sdk/Pagination/CursorSequence.cs new file mode 100644 index 00000000..b6ae47d0 --- /dev/null +++ b/src/OpenCode.Sdk/Pagination/CursorSequence.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; + +namespace OpenCode.Sdk; + +/// +/// The lazy cursor traversal a generated Enumerate*Async companion returns. Enumerating the +/// sequence itself yields the items of every page in order; yields the same +/// traversal's page envelopes. Both doors drive the same traversal independently, so each +/// enumeration issues its own requests, and an API error surfaces when its page is reached. +/// +/// The generated response envelope one page request returns. +/// The item type the pages carry. +public sealed class CursorSequence : IAsyncEnumerable + where TPage : OpenCodeResponse +{ + private readonly Func> _pages; + private readonly Func> _items; + private readonly CancellationToken _sequenceToken; + + internal CursorSequence( + Func> pages, + Func> items, + CancellationToken sequenceToken) + { + ArgumentNullException.ThrowIfNull(pages); + ArgumentNullException.ThrowIfNull(items); + + _pages = pages; + _items = items; + _sequenceToken = sequenceToken; + Pages = new PageSequence(this); + } + + /// + /// Gets the traversal as page envelopes: every response the traversal requests, including its + /// status, cursor, and the rest of the envelope. Enumerating it walks the pages again from the + /// first request. + /// + public IAsyncEnumerable Pages { get; } + + /// + /// Gets an enumerator over the items of every page, in page order. The token supplied here and + /// the token supplied to the companion are both observed. + /// + /// The cancellation token. + /// The item enumerator. + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => + EnumerateItemsAsync(_sequenceToken).GetAsyncEnumerator(cancellationToken); + + private async IAsyncEnumerable EnumerateItemsAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var page in _pages(cancellationToken).ConfigureAwait(false)) + { + foreach (var item in _items(page)) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return item; + } + } + } + + /// + /// The page door. It owns no traversal of its own: every enumeration asks the sequence for a + /// fresh page walk, which is what makes the two doors independent. + /// + private sealed class PageSequence(CursorSequence sequence) : IAsyncEnumerable + { + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => + sequence._pages(sequence._sequenceToken).GetAsyncEnumerator(cancellationToken); + } +} diff --git a/src/OpenCode.Sdk/Sessions/SessionClient.cs b/src/OpenCode.Sdk/Sessions/SessionClient.cs index 0d03c17f..c29a04dc 100644 --- a/src/OpenCode.Sdk/Sessions/SessionClient.cs +++ b/src/OpenCode.Sdk/Sessions/SessionClient.cs @@ -252,14 +252,14 @@ public virtual Task ListMessagesAsync(MessageListRequest? r } /// - /// Enumerates the items returned by 'GET /api/session/{sessionID}/message' by following each opaque next cursor. + /// Enumerates the items returned by 'GET /api/session/{sessionID}/message' by following each opaque next cursor; every filter of the first request rides each continuation. /// /// The request shaping the first page. /// The cancellation token. - /// The 'ISessionMessageInfo' sequence. + /// The 'ISessionMessageInfo' sequence, whose Pages expose each 'MessageListResponse'. /// The API returned a declared error status (declared: 400, 401, 404, 500); pagination API errors always throw. /// The server could not be reached or returned a malformed success body. - public virtual IAsyncEnumerable EnumerateMessagesAsync(MessageListRequest? request = null, CancellationToken cancellationToken = default) + public virtual CursorSequence EnumerateMessagesAsync(MessageListRequest? request = null, CancellationToken cancellationToken = default) { return CursorPaginator.EnumerateAsync(ListMessagesAsync, request, MessageListResponseAdapter.Instance, cancellationToken); } diff --git a/src/OpenCode.Sdk/Sessions/SessionListRequest.cs b/src/OpenCode.Sdk/Sessions/SessionListRequest.cs index 8e948157..53c9462f 100644 --- a/src/OpenCode.Sdk/Sessions/SessionListRequest.cs +++ b/src/OpenCode.Sdk/Sessions/SessionListRequest.cs @@ -2,23 +2,15 @@ // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. namespace OpenCode.Sdk; /// -/// Shapes the 'GET /api/session' query. +/// Shapes the 'GET /api/session' query. 'limit': Maximum number of sessions to return. Defaults to the newest 50 sessions. 'order': Session order for the first page. Use desc for newest first or asc for oldest first. 'cursor': Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. /// -public sealed record SessionListRequest +public sealed record SessionListRequest : ListRequest { /// /// Gets the 'workspace' query value; the server default applies when unset. /// public string? Workspace { get; init; } /// - /// Maximum number of sessions to return. Defaults to the newest 50 sessions. - /// - public string? Limit { get; init; } - /// - /// Session order for the first page. Use desc for newest first or asc for oldest first. - /// - public ListOrder? Order { get; init; } - /// /// Gets the 'search' query value; the server default applies when unset. /// public string? Search { get; init; } @@ -38,8 +30,4 @@ public sealed record SessionListRequest /// Gets the 'subpath' query value; the server default applies when unset. /// public string? Subpath { get; init; } - /// - /// Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. - /// - public string? Cursor { get; init; } } diff --git a/src/OpenCode.Sdk/Sessions/SessionsClient.cs b/src/OpenCode.Sdk/Sessions/SessionsClient.cs index e871db7a..7ad37ef5 100644 --- a/src/OpenCode.Sdk/Sessions/SessionsClient.cs +++ b/src/OpenCode.Sdk/Sessions/SessionsClient.cs @@ -1,6 +1,7 @@ // Generated by OpenCode.Sdk.Tools from spec/openapi.json. // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.Pagination; using OpenCode.Sdk.Internal.ResponseAdapters; using OpenCode.Sdk.Internal.Serialization; using OpenCode.Sdk.Models; @@ -99,6 +100,19 @@ public virtual Task ListSessionsAsync(SessionListRequest? r return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Sessions.ListSessions(request), SessionListResponseAdapter.Instance, requestOptions, cancellationToken); } + /// + /// Enumerates the items returned by 'GET /api/session' by following each opaque next cursor; every filter of the first request rides each continuation. + /// + /// The request shaping the first page. + /// The cancellation token. + /// The 'SessionInfo' sequence, whose Pages expose each 'SessionListResponse'. + /// The API returned a declared error status (declared: 400, 401); pagination API errors always throw. + /// The server could not be reached or returned a malformed success body. + public virtual CursorSequence EnumerateSessionsAsync(SessionListRequest? request = null, CancellationToken cancellationToken = default) + { + return CursorPaginator.EnumerateAsync(ListSessionsAsync, request, SessionListResponseAdapter.Instance, cancellationToken); + } + /// /// Import session. Import a projected session transcript at the requested location. If parentID is supplied, the parent session must already exist; import parents before children. /// diff --git a/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/PaginationBenchmarks.cs b/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/PaginationBenchmarks.cs index d2e55f50..b4bc7403 100644 --- a/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/PaginationBenchmarks.cs +++ b/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/PaginationBenchmarks.cs @@ -45,6 +45,7 @@ public async Task SetupAsync() _session = _client.Sessions.GetSessionClient("ses_bench0000000000000000001"); if (await EnumerateMessagesAsync().ConfigureAwait(false) != PageCount * ItemsPerPage + || await EnumerateMessagePagesAsync().ConfigureAwait(false) != PageCount * ItemsPerPage || await ListPagesManuallyAsync().ConfigureAwait(false) != PageCount * ItemsPerPage) { throw new InvalidOperationException("The canned pages did not traverse to the expected item count."); @@ -57,7 +58,7 @@ public async Task EnumerateMessagesAsync() { var session = _session!; var count = 0; - await foreach (var item in session.EnumerateMessagesAsync().ConfigureAwait(false)) + await foreach (var item in session.EnumerateMessagesAsync().WithCancellation(CancellationToken.None).ConfigureAwait(false)) { _ = item; count++; @@ -66,6 +67,20 @@ public async Task EnumerateMessagesAsync() return count; } + /// The same traversal read through the sequence's page door instead of its items. + [Benchmark] + public async Task EnumerateMessagePagesAsync() + { + var session = _session!; + var count = 0; + await foreach (var page in session.EnumerateMessagesAsync().Pages.WithCancellation(CancellationToken.None).ConfigureAwait(false)) + { + count += page.Messages.Count; + } + + return count; + } + /// The control: the same pages through explicit page calls and a hand-written loop. [Benchmark] public async Task ListPagesManuallyAsync() diff --git a/tests/OpenCode.Sdk.Tests/Sessions/SessionClientPaginationTests.cs b/tests/OpenCode.Sdk.Tests/Sessions/SessionClientPaginationTests.cs index 5b47f3a4..8f04beaa 100644 --- a/tests/OpenCode.Sdk.Tests/Sessions/SessionClientPaginationTests.cs +++ b/tests/OpenCode.Sdk.Tests/Sessions/SessionClientPaginationTests.cs @@ -1,3 +1,5 @@ +using OpenCode.Sdk.Models; + namespace OpenCode.Sdk.Tests; public sealed class SessionClientPaginationTests @@ -29,21 +31,221 @@ public async Task EnumerateMessagesAsync_Should_Use_The_Virtual_Page_Method_And_ await Assert.That(client.Requests[1]?.Cursor).IsEqualTo(string.Empty); } - private static MessageListResponse Page(string? next) => new() + [Test] + public async Task EnumerateMessagesAsync_Should_Yield_Every_Buffered_Item_In_Page_Order() + { + var client = new StubSessionClient( + Page("cur_1", Message("msg_1"), Message("msg_2")), + Page(next: null, Message("msg_3"))); + var received = new List(); + + await foreach (var message in client.EnumerateMessagesAsync()) + { + received.Add(((SessionMessageUser)message).Id); + } + + await Assert.That(received.SequenceEqual(["msg_1", "msg_2", "msg_3"], StringComparer.Ordinal)).IsTrue(); + } + + [Test] + public async Task Pages_Should_Yield_Each_Page_Envelope_Of_The_Same_Traversal() + { + var client = new StubSessionClient( + Page("cur_1", Message("msg_1")), + Page(next: null, Message("msg_2"))); + var received = new List(); + + await foreach (var page in client.EnumerateMessagesAsync().Pages.WithCancellation(CancellationToken.None)) + { + received.Add(page); + } + + await Assert.That(received.Count).IsEqualTo(2); + await Assert.That(received[0].Cursor.Next).IsEqualTo("cur_1"); + await Assert.That(received[1].Cursor.Next).IsNull(); + await Assert.That(received.All(static page => page.Status == 200)).IsTrue(); + await Assert.That(client.Requests.Count).IsEqualTo(2); + } + + [Test] + public async Task Pages_Should_Send_Its_Own_Requests_When_The_Sequence_Is_Enumerated_Twice() + { + var client = new StubSessionClient( + Page("cur_1", Message("msg_1")), + Page(next: null, Message("msg_2")), + Page("cur_1", Message("msg_1")), + Page(next: null, Message("msg_2"))); + var sequence = client.EnumerateMessagesAsync(); + var items = 0; + var pages = 0; + + await foreach (var _ in sequence) + { + items++; + } + + await foreach (var _ in sequence.Pages.WithCancellation(CancellationToken.None)) + { + pages++; + } + + await Assert.That(items).IsEqualTo(2); + await Assert.That(pages).IsEqualTo(2); + await Assert.That(client.Requests.Count).IsEqualTo(4); + } + + [Test] + public async Task GetAsyncEnumerator_Should_Observe_The_Token_Given_To_The_Companion() + { + using var cancellation = new CancellationTokenSource(); + var client = new StubSessionClient(Page(next: null, Message("msg_1"), Message("msg_2"))); + await using var enumerator = client + .EnumerateMessagesAsync(cancellationToken: cancellation.Token) + .GetAsyncEnumerator(); + + await Assert.That(await enumerator.MoveNextAsync()).IsTrue(); + await cancellation.CancelAsync(); + + await Assert.That(client.Tokens[0].IsCancellationRequested).IsTrue(); + _ = await Assert.That(async () => _ = await enumerator.MoveNextAsync()).Throws(); + } + + [Test] + public async Task GetAsyncEnumerator_Should_Observe_The_Token_Given_To_The_Enumerator() + { + using var cancellation = new CancellationTokenSource(); + var client = new StubSessionClient(Page(next: null, Message("msg_1"), Message("msg_2"))); + await using var enumerator = client + .EnumerateMessagesAsync(cancellationToken: CancellationToken.None) + .GetAsyncEnumerator(cancellation.Token); + + await Assert.That(await enumerator.MoveNextAsync()).IsTrue(); + await cancellation.CancelAsync(); + + await Assert.That(client.Tokens[0].IsCancellationRequested).IsTrue(); + _ = await Assert.That(async () => _ = await enumerator.MoveNextAsync()).Throws(); + } + + [Test] + public async Task Pages_Should_Surface_The_Failure_Of_The_Page_That_Raised_It() + { + var failure = new InvalidOperationException("page two failed"); + var client = new FailingSessionClient(Page("cur_1", Message("msg_1")), failure); + await using var enumerator = client.EnumerateMessagesAsync().Pages.GetAsyncEnumerator(); + + await Assert.That(await enumerator.MoveNextAsync()).IsTrue(); + await Assert.That(enumerator.Current.Cursor.Next).IsEqualTo("cur_1"); + + var thrown = await Assert.That(async () => _ = await enumerator.MoveNextAsync()).Throws(); + await Assert.That(thrown).IsSameReferenceAs(failure); + } + + [Test] + public async Task EnumerateSessionsAsync_Should_Carry_Every_Filter_And_Drop_Order_On_A_Continuation() + { + var client = new StubSessionsClient(SessionPage("cur_1"), SessionPage(next: null)); + var initialRequest = new SessionListRequest + { + Limit = "3", + Order = ListOrder.Descending, + Search = "needle", + Project = "prj_1", + }; + + await foreach (var _ in client.EnumerateSessionsAsync(initialRequest)) + { + // The traversal itself is the subject; the canned pages carry no sessions. + } + + await Assert.That(client.Requests.Count).IsEqualTo(2); + await Assert.That(ReferenceEquals(client.Requests[0], initialRequest)).IsTrue(); + await Assert.That(client.Requests[1]?.Limit).IsEqualTo("3"); + await Assert.That(client.Requests[1]?.Search).IsEqualTo("needle"); + await Assert.That(client.Requests[1]?.Project).IsEqualTo("prj_1"); + await Assert.That(client.Requests[1]?.Cursor).IsEqualTo("cur_1"); + await Assert.That(client.Requests[1]?.Order).IsNull(); + } + + [Test] + public async Task EnumerateSessionsAsync_Should_Start_A_Continuation_From_An_Absent_Request() + { + var client = new StubSessionsClient(SessionPage("cur_1"), SessionPage(next: null)); + + await foreach (var _ in client.EnumerateSessionsAsync().Pages.WithCancellation(CancellationToken.None)) + { + // Only the continuation request shape is under test. + } + + await Assert.That(client.Requests.Count).IsEqualTo(2); + await Assert.That(client.Requests[0]).IsNull(); + await Assert.That(client.Requests[1]?.Cursor).IsEqualTo("cur_1"); + await Assert.That(client.Requests[1]?.Limit).IsNull(); + await Assert.That(client.Requests[1]?.Order).IsNull(); + } + + private static MessageListResponse Page(string? next, params ISessionMessageInfo[] messages) => new() { Status = 200, - Messages = [], + Messages = messages, Cursor = new ListCursor { Next = next, }, }; + private static SessionListResponse SessionPage(string? next) => new() + { + Status = 200, + Sessions = [], + Cursor = new ListCursor { Next = next, }, + }; + + private static SessionMessageUser Message(string id) => new() + { + Id = id, + Text = id, + Time = new SessionMessageUserTime { Created = 0, }, + }; + private sealed class StubSessionClient(params MessageListResponse[] pages) : SessionClient { private readonly Queue _pages = new(pages); public List Requests { get; } = []; + public List Tokens { get; } = []; + + public override Task ListMessagesAsync(MessageListRequest? request = null, + OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) + { + Requests.Add(request); + Tokens.Add(cancellationToken); + return Task.FromResult(_pages.Dequeue()); + } + } + + private sealed class FailingSessionClient(MessageListResponse firstPage, Exception failure) : SessionClient + { + private bool _served; + public override Task ListMessagesAsync(MessageListRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) + { + if (_served) + { + return Task.FromException(failure); + } + + _served = true; + return Task.FromResult(firstPage); + } + } + + private sealed class StubSessionsClient(params SessionListResponse[] pages) : SessionsClient + { + private readonly Queue _pages = new(pages); + + public List Requests { get; } = []; + + public override Task ListSessionsAsync(SessionListRequest? request = null, + OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) { Requests.Add(request); return Task.FromResult(_pages.Dequeue()); diff --git a/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientContractTests.cs b/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientContractTests.cs index ca7235f5..5c98a112 100644 --- a/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientContractTests.cs +++ b/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientContractTests.cs @@ -41,6 +41,110 @@ await Assert.That(scenario.Requests.Single().RequestUri!.AbsoluteUri).IsEqualTo( "http://localhost:4096/api/session?limit=5&order=desc&search=a%20b&parentID=null&cursor=cur_1"); } + [Test] + public async Task EnumerateSessionsAsync_Should_Carry_Every_Filter_Through_The_Opaque_Next_Cursors() + { + const string firstNext = "cur_next_1"; + var payload = new FixtureLoader().LoadJson("Serialization.known-session.json"); + var responses = new Queue( + [ + WireBodyData.Page(payload, next: firstNext), + WireBodyData.Page(payload), + ]); + using var scenario = ContractScenario.Responding(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responses.Dequeue()), + }); + var sequence = scenario.Client.Sessions.EnumerateSessionsAsync( + new SessionListRequest + { + Limit = "1", + Order = ListOrder.Descending, + Search = "a b", + ParentId = SessionParentFilter.RootOnly, + }, + CancellationToken.None); + + await Assert.That(scenario.Requests).IsEmpty(); + + var received = new List(); + await foreach (var session in sequence.WithCancellation(CancellationToken.None)) + { + received.Add(session); + } + + await Assert.That(received.Count).IsEqualTo(2); + await Assert.That(received.All(static session => session.Id == "ses_100")).IsTrue(); + await Assert + .That(scenario.Requests.Select(static request => request.RequestUri!.AbsoluteUri).SequenceEqual( + [ + "http://localhost:4096/api/session?limit=1&order=desc&search=a%20b&parentID=null", + $"http://localhost:4096/api/session?limit=1&search=a%20b&parentID=null&cursor={firstNext}", + ], + StringComparer.Ordinal)) + .IsTrue(); + } + + [Test] + public async Task EnumerateSessionsAsync_Should_Expose_Each_Page_Envelope_Through_Pages() + { + var payload = new FixtureLoader().LoadJson("Serialization.known-session.json"); + var responses = new Queue( + [ + WireBodyData.Page(payload, next: "cur_next_1"), + WireBodyData.Page(payload), + ]); + using var scenario = ContractScenario.Responding(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responses.Dequeue()), + }); + var pages = new List(); + + await foreach (var page in scenario.Client.Sessions + .EnumerateSessionsAsync(cancellationToken: CancellationToken.None) + .Pages + .WithCancellation(CancellationToken.None)) + { + pages.Add(page); + } + + await Assert.That(pages.Count).IsEqualTo(2); + await Assert.That(pages[0].Status).IsEqualTo(200); + await Assert.That(pages[0].Cursor.Next).IsEqualTo("cur_next_1"); + await Assert.That(pages[0].Sessions.Single().Title).IsEqualTo("Fix the build"); + await Assert.That(pages[1].Cursor.Next).IsNull(); + await Assert.That(scenario.Requests.Count).IsEqualTo(2); + } + + [Test] + public async Task EnumerateSessionsAsync_Should_Throw_The_Declared_Error_When_A_Later_Page_Fails() + { + var payload = new FixtureLoader().LoadJson("Serialization.known-session.json"); + var responses = new Queue<(HttpStatusCode Status, string Body)>( + [ + (HttpStatusCode.OK, WireBodyData.Page(payload, next: "cur_invalid")), + (HttpStatusCode.BadRequest, WireBodyData.InvalidCursorError), + ]); + using var scenario = ContractScenario.Responding(_ => + { + var response = responses.Dequeue(); + return new HttpResponseMessage(response.Status) { Content = new StringContent(response.Body), }; + }); + await using var enumerator = scenario.Client.Sessions + .EnumerateSessionsAsync(cancellationToken: CancellationToken.None) + .GetAsyncEnumerator(); + + await Assert.That(await enumerator.MoveNextAsync()).IsTrue(); + await Assert.That(enumerator.Current.Id).IsEqualTo("ses_100"); + + var exception = await Assert + .That(async () => _ = await enumerator.MoveNextAsync()) + .Throws(); + + await Assert.That(exception?.Error).IsTypeOf(); + await Assert.That(scenario.Requests.Count).IsEqualTo(2); + } + [Test] [Arguments(" ")] [Arguments(".")] diff --git a/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientLiveTests.cs b/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientLiveTests.cs index 0c4f935e..46592545 100644 --- a/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientLiveTests.cs +++ b/tests/OpenCode.Sdk.Tests/Sessions/SessionsClientLiveTests.cs @@ -138,6 +138,74 @@ await Assert.That(response.Stats.Models.Any(model => } } + [Test] + [Timeout(180_000)] + public async Task EnumerateSessionsAsync_Should_Cross_A_Page_Boundary_Carrying_Its_Filter( + CancellationToken cancellationToken) + { + using var workspace = server.CreateWorkspace(); + using var client = server.CreateClient(new LocationSelector { Directory = workspace.Path }); + var first = await CreateOwnedSessionAsync(client, workspace.Path, "session-pagination-live-1", cancellationToken); + var second = await CreateOwnedSessionAsync(client, workspace.Path, "session-pagination-live-2", cancellationToken); + var firstSession = client.Sessions.GetSessionClient(first.Id); + var secondSession = client.Sessions.GetSessionClient(second.Id); + var cleanup = CreateCleanup(firstSession, first.Id); + cleanup.Own("second session removal", token => RemoveOwnedSessionAsync(secondSession, second.Id, token)); + Exception? primaryFailure = null; + + try + { + // A one-item page forces continuations, and the project filter must ride every one of + // them: dropping it would surface sessions from the other tests sharing this server. + var request = new SessionListRequest + { + Limit = "1", + Order = ListOrder.Ascending, + Project = first.ProjectId, + }; + var pages = new List(); + await foreach (var page in client.Sessions + .EnumerateSessionsAsync(request, cancellationToken) + .Pages + .WithCancellation(cancellationToken)) + { + pages.Add(page); + } + + var enumerated = new List(); + await foreach (var session in client.Sessions.EnumerateSessionsAsync(request, cancellationToken)) + { + enumerated.Add(session); + } + + await Assert.That(pages.Count).IsGreaterThan(1); + await Assert.That(pages.All(static page => page is { Status: 200, IsError: false })).IsTrue(); + await Assert.That(pages[^1].Cursor.Next).IsNull(); + await Assert + .That(pages + .SelectMany(static page => page.Sessions) + .Select(static session => session.Id) + .SequenceEqual(enumerated.Select(static session => session.Id), StringComparer.Ordinal)) + .IsTrue(); + await Assert.That(enumerated.All(session => session.ProjectId == first.ProjectId)).IsTrue(); + await Assert.That(enumerated.Any(session => session.Id == first.Id)).IsTrue(); + await Assert.That(enumerated.Any(session => session.Id == second.Id)).IsTrue(); + + Console.WriteLine( + "session-pagination-live: pages=" + Number(pages.Count) + + " sessions=" + Number(enumerated.Count) + + " project=" + first.ProjectId); + } + catch (Exception exception) + { + primaryFailure = exception; + } + finally + { + await cleanup.CompleteAsync(primaryFailure); + } + } + private static async Task CreateOwnedSessionAsync( OpenCodeClient client, string directory, diff --git a/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt index 806b3ce1..57b89192 100644 --- a/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt +++ b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt @@ -76,6 +76,12 @@ public virtual System.Threading.Tasks.Task RemoveCredentialAsync(string credentialId, OpenCode.Sdk.CredentialRemoveRequest? request = null, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task UpdateCredentialAsync(string credentialId, OpenCode.Sdk.Models.CredentialUpdatePatchRequest request, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } } + public sealed class CursorSequence : System.Collections.Generic.IAsyncEnumerable + where TPage : OpenCode.Sdk.OpenCodeResponse + { + public System.Collections.Generic.IAsyncEnumerable Pages { get; } + public System.Collections.Generic.IAsyncEnumerator GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default) { } + } public class DebugClient { protected DebugClient() { } @@ -1395,7 +1401,7 @@ public virtual System.Threading.Tasks.Task CreateFormAsync(OpenCode.Sdk.Models.SessionFormCreateRequest request, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task CreatePermissionAsync(OpenCode.Sdk.Models.SessionPermissionCreateRequest request, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task DeleteInboxCancelAsync(string inboxId, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } - public virtual System.Collections.Generic.IAsyncEnumerable EnumerateMessagesAsync(OpenCode.Sdk.MessageListRequest? request = null, System.Threading.CancellationToken cancellationToken = default) { } + public virtual OpenCode.Sdk.CursorSequence EnumerateMessagesAsync(OpenCode.Sdk.MessageListRequest? request = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task GetContextAsync(OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task GetExportAsync(OpenCode.Sdk.SessionExportRequest? request = null, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task GetFormAsync(string formId, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } @@ -1569,13 +1575,10 @@ public required OpenCode.Sdk.Models.SessionInterruptResponse Interrupt { get; init; } protected override bool PrintMembers(System.Text.StringBuilder builder) { } } - public sealed class SessionListRequest : System.IEquatable + public sealed class SessionListRequest : OpenCode.Sdk.ListRequest, System.IEquatable { public SessionListRequest() { } - public string? Cursor { get; init; } public string? Directory { get; init; } - public string? Limit { get; init; } - public OpenCode.Sdk.ListOrder? Order { get; init; } public OpenCode.Sdk.SessionParentFilter? ParentId { get; init; } public string? Project { get; init; } public string? Search { get; init; } @@ -1721,6 +1724,7 @@ { protected SessionsClient() { } public virtual System.Threading.Tasks.Task CreateSessionAsync(OpenCode.Sdk.Models.SessionCreateRequest? request = null, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } + public virtual OpenCode.Sdk.CursorSequence EnumerateSessionsAsync(OpenCode.Sdk.SessionListRequest? request = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task GetActiveAsync(OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual OpenCode.Sdk.SessionClient GetSessionClient(string sessionId) { } public virtual System.Threading.Tasks.Task GetStatsAsync(OpenCode.Sdk.SessionStatsRequest? request = null, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs index f4eaf2b0..dbb69005 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs @@ -246,7 +246,7 @@ await Assert var list = sessions.Operations.Single(static operation => operation.MethodName == "ListSessionsAsync"); await Assert.That(list.QueryRequest!.TypeName).IsEqualTo("SessionListRequest"); - await Assert.That(list.QueryRequest.DerivesFromListRequest).IsFalse(); + await Assert.That(list.QueryRequest.DerivesFromListRequest).IsTrue(); await Assert .That(list .QueryRequest.Properties.Select(static property => property.PropertyName) @@ -254,6 +254,12 @@ await Assert ["Workspace", "Limit", "Order", "Search", "ParentId", "Directory", "Project", "Subpath", "Cursor"], StringComparer.Ordinal)) .IsTrue(); + await Assert + .That(list + .QueryRequest.Properties.Where(static property => property.IsInherited) + .Select(static property => property.PropertyName) + .SequenceEqual(["Limit", "Order", "Cursor"], StringComparer.Ordinal)) + .IsTrue(); await Assert.That(list.Envelope!.Kind).IsEqualTo(EnvelopeKind.CursorList); await Assert.That(list.Envelope.PayloadName).IsEqualTo("Sessions"); await Assert.That(list.Envelope.PayloadType).IsTypeOf(); @@ -497,7 +503,7 @@ await Assert } [Test] - public async Task Bind_Should_Recognize_Only_The_Selected_ListRequest_Cursor_Operation_As_Paginated() + public async Task Bind_Should_Recognize_Every_Pinned_ListRequest_Cursor_Operation_As_Paginated() { var plan = await new BindingTestHost().BindPinnedAsync(); var session = plan.Clients.Single(static client => client.Name == "SessionClient"); @@ -508,11 +514,19 @@ public async Task Bind_Should_Recognize_Only_The_Selected_ListRequest_Cursor_Ope await Assert.That(pagination).IsNotNull(); await Assert.That(pagination.MethodName).IsEqualTo("EnumerateMessagesAsync"); await Assert.That(pagination.RequestTypeName).IsEqualTo("MessageListRequest"); + await Assert.That(pagination.PageTypeName).IsEqualTo("MessageListResponse"); await Assert.That(pagination.ItemTypeName).IsEqualTo("ISessionMessageInfo"); await Assert.That(pagination.PayloadName).IsEqualTo("Messages"); - await Assert - .That(sessions.Operations.Single(static operation => operation.MethodName == "ListSessionsAsync").Pagination) - .IsNull(); + + var sessionPagination = sessions.Operations + .Single(static operation => operation.MethodName == "ListSessionsAsync") + .Pagination; + await Assert.That(sessionPagination).IsNotNull(); + await Assert.That(sessionPagination.MethodName).IsEqualTo("EnumerateSessionsAsync"); + await Assert.That(sessionPagination.RequestTypeName).IsEqualTo("SessionListRequest"); + await Assert.That(sessionPagination.PageTypeName).IsEqualTo("SessionListResponse"); + await Assert.That(sessionPagination.ItemTypeName).IsEqualTo("SessionInfo"); + await Assert.That(sessionPagination.PayloadName).IsEqualTo("Sessions"); } [Test] @@ -783,7 +797,7 @@ public async Task Bind_Should_Refuse_An_Optional_Request_Body() } [Test] - public async Task Bind_Should_Bind_Optional_Nullable_Query_Parameters_Into_A_Flat_Query_Request() + public async Task Bind_Should_Bind_Optional_Nullable_Query_Parameters_Beside_The_Inherited_Spine() { var document = await BindingTestHost.IngestAsync(WidgetListScenario(operation => operation .Parameter("limit", "query", QueryScenarioData.NullableString) @@ -797,7 +811,7 @@ public async Task Bind_Should_Bind_Optional_Nullable_Query_Parameters_Into_A_Fla var list = plan.Clients.Single(static client => client.Role == ClientRole.Collection).Operations.Single(); await Assert.That(list.QueryRequest).IsNotNull(); await Assert.That(list.QueryRequest!.TypeName).IsEqualTo("WidgetListRequest"); - await Assert.That(list.QueryRequest.DerivesFromListRequest).IsFalse(); + await Assert.That(list.QueryRequest.DerivesFromListRequest).IsTrue(); await Assert .That(list .QueryRequest.Properties.Select(static property => property.WireName) @@ -819,7 +833,11 @@ await Assert QueryValueKind.SessionParentFilter, ])) .IsTrue(); - await Assert.That(list.QueryRequest.Properties.All(static property => !property.IsInherited)).IsTrue(); + await Assert + .That(list + .QueryRequest.Properties.Select(static property => property.IsInherited) + .SequenceEqual([true, true, true, false, false])) + .IsTrue(); await Assert.That(list.Parameters).IsEmpty(); } @@ -863,7 +881,7 @@ await Assert } [Test] - public async Task Bind_Should_Derive_The_Query_Request_From_The_List_Request_Base_When_The_Trio_Matches_Exactly() + public async Task Bind_Should_Derive_The_Query_Request_From_The_List_Request_Base_When_The_Query_Is_Only_The_Trio() { var document = await BindingTestHost.IngestAsync(WidgetListScenario(operation => operation .Parameter("limit", "query", QueryScenarioData.NullableString) @@ -884,13 +902,29 @@ await Assert } [Test] - public async Task Bind_Should_Keep_The_Query_Request_Flat_When_The_Trio_Has_Extra_Parameters() + public async Task Bind_Should_Keep_The_Query_Request_Flat_When_A_Spine_Parameter_Carries_An_Unadmitted_Schema() { var document = await BindingTestHost.IngestAsync(WidgetListScenario(operation => operation .Parameter("limit", "query", QueryScenarioData.NullableString) + .Parameter("order", "query", schema => schema.AnyOf( + branch => branch.Type("string").Enum("asc", "desc", "shuffled"), + branch => branch.Type("null"))) + .Parameter("cursor", "query", QueryScenarioData.NullableString))); + + var plan = BindWidgets(document); + + var list = plan.Clients.Single(static client => client.Role == ClientRole.Collection).Operations.Single(); + await Assert.That(list.QueryRequest!.DerivesFromListRequest).IsFalse(); + await Assert.That(list.QueryRequest.Properties.All(static property => !property.IsInherited)).IsTrue(); + } + + [Test] + public async Task Bind_Should_Keep_The_Query_Request_Flat_When_A_Spine_Parameter_Is_Declared_Required() + { + var document = await BindingTestHost.IngestAsync(WidgetListScenario(operation => operation + .Parameter("limit", "query", schema => schema.Type("string"), required: true) .Parameter("order", "query", QueryScenarioData.NullableOrderEnum) - .Parameter("cursor", "query", QueryScenarioData.NullableString) - .Parameter("search", "query", QueryScenarioData.NullableString))); + .Parameter("cursor", "query", QueryScenarioData.NullableString))); var plan = BindWidgets(document); @@ -957,6 +991,59 @@ await Assert .IsTrue(); } + [Test] + public async Task Bind_Should_Admit_Cursor_Pagination_When_Filters_Ride_The_Spine() + { + var document = await BindingTestHost.IngestAsync(CursorListScenario(configure: operation => operation + .Parameter("limit", "query", QueryScenarioData.NullableString) + .Parameter("order", "query", QueryScenarioData.NullableOrderEnum) + .Parameter("cursor", "query", QueryScenarioData.NullableString) + .Parameter("search", "query", QueryScenarioData.NullableString))); + + var plan = BindWidgets(document); + + var list = plan.Clients.Single(static client => client.Role == ClientRole.Collection).Operations.Single(); + await Assert.That(list.QueryRequest!.DerivesFromListRequest).IsTrue(); + await Assert.That(list.Pagination).IsNotNull(); + await Assert.That(list.Pagination.MethodName).IsEqualTo("EnumerateWidgetsAsync"); + await Assert.That(list.Pagination.RequestTypeName).IsEqualTo("WidgetListRequest"); + await Assert.That(list.Pagination.PageTypeName).IsEqualTo("WidgetListResponse"); + await Assert.That(list.Pagination.ItemTypeName).IsEqualTo("WidgetInfo"); + } + + [Test] + public async Task Bind_Should_Leave_Cursor_Pagination_Absent_When_The_Spine_Is_Incomplete() + { + var document = await BindingTestHost.IngestAsync(CursorListScenario(configure: operation => operation + .Parameter("limit", "query", QueryScenarioData.NullableString) + .Parameter("cursor", "query", QueryScenarioData.NullableString))); + + var plan = BindWidgets(document); + + var list = plan.Clients.Single(static client => client.Role == ClientRole.Collection).Operations.Single(); + await Assert.That(list.QueryRequest!.DerivesFromListRequest).IsFalse(); + await Assert.That(list.Pagination).IsNull(); + } + + [Test] + public async Task Bind_Should_Refuse_Cursor_Pagination_When_A_Filter_Riding_The_Spine_Is_Required() + { + var document = await BindingTestHost.IngestAsync(CursorListScenario(configure: operation => operation + .Parameter("limit", "query", QueryScenarioData.NullableString) + .Parameter("order", "query", QueryScenarioData.NullableOrderEnum) + .Parameter("cursor", "query", QueryScenarioData.NullableString) + .Parameter("search", "query", schema => schema.Type("string"), required: true))); + + var exception = Assert.Throws(() => _ = BindWidgets(document)); + + await Assert + .That(exception.Errors.Any(static error => error.Category == BindingErrorCategory.Operation + && error.Problem.Contains( + "cursor pagination cannot continue a query that declares a required parameter", + StringComparison.Ordinal))) + .IsTrue(); + } + [Test] public async Task Bind_Should_Refuse_A_Cursor_Paginator_Whose_Enumeration_Name_Cannot_Be_Derived() { diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt index 80382a0f..d727089f 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt @@ -162,14 +162,14 @@ public class WidgetClient } /// - /// Enumerates the items returned by 'GET /api/widget/{widgetID}/item' by following each opaque next cursor. + /// Enumerates the items returned by 'GET /api/widget/{widgetID}/item' by following each opaque next cursor; every filter of the first request rides each continuation. /// /// The request shaping the first page. /// The cancellation token. - /// The 'ExampleItem' sequence. + /// The 'ExampleItem' sequence, whose Pages expose each 'WidgetItemListResponse'. /// The API returned a declared error status (declared: 404); pagination API errors always throw. /// The server could not be reached or returned a malformed success body. - public virtual IAsyncEnumerable EnumerateItemsAsync(ItemListRequest? request = null, CancellationToken cancellationToken = default) + public virtual CursorSequence EnumerateItemsAsync(ItemListRequest? request = null, CancellationToken cancellationToken = default) { return CursorPaginator.EnumerateAsync(ListItemsAsync, request, WidgetItemListResponseAdapter.Instance, cancellationToken); } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt index 4a4ae69e..c933c952 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt @@ -188,14 +188,14 @@ internal sealed class WidgetItemListResponseAdapter : ResponseAdapter - /// Creates a continuation request carrying the initial limit and opaque next cursor. + /// Creates a continuation request carrying every initial filter beside the opaque next cursor, with the first-page-only order dropped. /// public ItemListRequest CreateNextRequest(ItemListRequest? initialRequest, string cursor) { ArgumentNullException.ThrowIfNull(cursor); - return new ItemListRequest + return (initialRequest ?? new())with { - Limit = initialRequest?.Limit, + Order = null, Cursor = cursor }; } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs index 45a0ec8d..92983ede 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs @@ -492,6 +492,7 @@ private static OperationPlan CreateItemListOperation() => { MethodName = "EnumerateItemsAsync", RequestTypeName = "ItemListRequest", + PageTypeName = "WidgetItemListResponse", ItemTypeName = "ExampleItem", PayloadName = "Items", }, diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ListRequestSpinePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ListRequestSpinePolicy.cs new file mode 100644 index 00000000..6f27a1a7 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ListRequestSpinePolicy.cs @@ -0,0 +1,39 @@ +using OpenCode.Sdk.Tools.Generator.Binding.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +/// +/// Decides which bound query properties the hand-written ListRequest base already +/// declares. The spine is recognized by inclusion: all three admitted members must be present +/// in their admitted shapes, while any further optional parameter binds beside them as its own +/// property, so a filtered list stays one request record with one continuation contract. A +/// missing member, or a member whose schema is not the admitted one, keeps the record flat. +/// +internal static class ListRequestSpinePolicy +{ + private static readonly string[] SpineWireNames = ["limit", "order", "cursor"]; + + /// Reports whether the bound query carries every admitted spine member. + public static bool CarriesSpine(IReadOnlyList properties) + { + ArgumentNullException.ThrowIfNull(properties); + + return Array.TrueForAll( + SpineWireNames, + wireName => properties.Any(property => + string.Equals(property.WireName, wireName, StringComparison.Ordinal) && IsSpineMember(property))); + } + + /// Reports whether one bound property is the base's own declaration rather than a filter. + public static bool IsSpineMember(QueryPropertyPlan property) + { + ArgumentNullException.ThrowIfNull(property); + + return property switch + { + { WireName: "limit" or "cursor", Kind: QueryValueKind.Text, IsRequired: false } => true, + { WireName: "order", Kind: QueryValueKind.ListOrder, IsRequired: false } => true, + _ => false, + }; + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PaginationPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PaginationPlan.cs index 1449b58f..5df7ccd9 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PaginationPlan.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PaginationPlan.cs @@ -7,6 +7,9 @@ internal sealed record PaginationPlan public required string RequestTypeName { get; init; } + /// Gets the generated response envelope the traversal yields as one page. + public required string PageTypeName { get; init; } + public required string ItemTypeName { get; init; } public required string PayloadName { get; init; } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/PaginationFacetBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/PaginationFacetBinder.cs index da4f3738..b0ddcdd2 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/PaginationFacetBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/PaginationFacetBinder.cs @@ -22,6 +22,7 @@ internal sealed class PaginationFacetBinder(OperationFacetContext context) { Kind: EnvelopeKind.CursorList, PayloadName: { } payloadName, + ResponseTypeName: { } pageTypeName, PayloadType: ListTypeReferencePlan { ElementType: NamedTypeReferencePlan { Name: { } itemTypeName } }, }) { @@ -44,6 +45,14 @@ internal sealed class PaginationFacetBinder(OperationFacetContext context) return _context.RefuseNull("cursor pagination cannot carry a declared header parameter"); } + // A continuation is the caller's first request with a new cursor, so the traversal must be + // able to start one from no request at all; a required filter has no value to carry there. + if (queryRequest.HasRequiredMember) + { + return _context.RefuseNull( + "cursor pagination cannot continue a query that declares a required parameter"); + } + var enumerationMethodName = OperationNamePolicy.EnumerationMethodName(methodName); if (enumerationMethodName is null) { @@ -58,6 +67,7 @@ internal sealed class PaginationFacetBinder(OperationFacetContext context) { MethodName = enumerationMethodName, RequestTypeName = queryRequest.TypeName, + PageTypeName = pageTypeName, ItemTypeName = itemTypeName, PayloadName = payloadName, }; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryRequestFacetBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryRequestFacetBinder.cs index 5c984d56..2cf9a953 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryRequestFacetBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryRequestFacetBinder.cs @@ -38,21 +38,24 @@ internal sealed class QueryRequestFacetBinder(OperationFacetContext context) return null; } - if (MatchesListRequestProfile(properties)) + var derivesFromListRequest = ListRequestSpinePolicy.CarriesSpine(properties); + if (derivesFromListRequest) { properties = [ - .. properties.Select(static property => property with - { - IsInherited = true - }) + .. properties.Select(static property => ListRequestSpinePolicy.IsSpineMember(property) + ? property with + { + IsInherited = true + } + : property) ]; } return new QueryRequestPlan { TypeName = OperationNamePolicy.RequestTypeName(_context.Operation), - DerivesFromListRequest = properties.Count > 0 && properties[0].IsInherited, + DerivesFromListRequest = derivesFromListRequest, Properties = properties, }; } @@ -159,16 +162,6 @@ private readonly record struct QueryEnumBinding(bool IsEnum, string? TypeName) }; } - /// - /// The fail-closed profile wall: an operation derives from the ListRequest base - /// only when its wire query parameters are exactly the optional cursor-pagination trio. - /// - private static bool MatchesListRequestProfile(List properties) => - properties.Count is 3 - && properties.Any(static property => property is { WireName: "limit", Kind: QueryValueKind.Text, IsRequired: false }) - && properties.Any(static property => property is { WireName: "order", Kind: QueryValueKind.ListOrder, IsRequired: false }) - && properties.Any(static property => property is { WireName: "cursor", Kind: QueryValueKind.Text, IsRequired: false }); - private QueryValueKind? ResolveQueryValueKind(SchemaNode value) { return _context.Resolve(value) switch diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReservedNamePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReservedNamePolicy.cs index 3a490bfc..59f75807 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReservedNamePolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReservedNamePolicy.cs @@ -34,6 +34,7 @@ internal static class ReservedNamePolicy /// public static readonly FrozenSet SpineTypeNames = FrozenSet.ToFrozenSet( [ + "CursorSequence", "ErrorBehavior", "IOpenCodeClientOptions", EffectStreamTypeNamePolicy.CauseMarkerInterface, diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/PaginationMethodEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/PaginationMethodEmitter.cs index 03abd29a..2a6e68cf 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/PaginationMethodEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/PaginationMethodEmitter.cs @@ -25,7 +25,10 @@ public static MethodDeclarationSyntax Emit(OperationPlan operation, EmissionMode SyntaxFactory.Argument(SyntaxFactory.IdentifierName(ReservedNamePolicy.CancellationTokenParameter))); return SyntaxFactory .MethodDeclaration( - TypeSyntaxEmitter.Generic("IAsyncEnumerable", TypeSyntaxEmitter.EmitNamed(pagination.ItemTypeName)), + TypeSyntaxEmitter.Generic( + "CursorSequence", + TypeSyntaxEmitter.EmitNamed(pagination.PageTypeName), + TypeSyntaxEmitter.EmitNamed(pagination.ItemTypeName)), pagination.MethodName) .WithModifiers(EmissionModifiers.Member(emission)) .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( @@ -65,12 +68,12 @@ private static SyntaxTriviaList EmitDocumentation(OperationPlan operation, Pagin "The server could not be reached or returned a malformed success body.")); return EmissionSyntax.MemberDocumentation( $"Enumerates the items returned by '{operation.HttpMethod.ToUpperInvariant()} {operation.RouteTemplate}' " - + "by following each opaque next cursor.", + + "by following each opaque next cursor; every filter of the first request rides each continuation.", [ new DocumentedParameter(ReservedNamePolicy.RequestParameter, "The request shaping the first page."), new DocumentedParameter(ReservedNamePolicy.CancellationTokenParameter, "The cancellation token."), ], - $"The '{pagination.ItemTypeName}' sequence.", + $"The '{pagination.ItemTypeName}' sequence, whose Pages expose each '{pagination.PageTypeName}'.", exceptions); } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs index 5a8b998a..a0cf642a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs @@ -233,23 +233,27 @@ private static MethodDeclarationSyntax EmitCreateNextRequest(OperationPlan opera var requestTypeName = operation.Pagination!.RequestTypeName; var statements = new List(); statements.AddRange(EmissionSyntax.ArgumentNullGuard("cursor")); - statements.Add(SyntaxFactory.ReturnStatement( - SyntaxFactory.ObjectCreationExpression(TypeSyntaxEmitter.EmitNamed(requestTypeName)) - .WithInitializer(SyntaxFactory.InitializerExpression( - SyntaxKind.ObjectInitializerExpression, - SyntaxFactory.SeparatedList( - [ - SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - SyntaxFactory.IdentifierName("Limit"), - SyntaxFactory.ConditionalAccessExpression( - SyntaxFactory.IdentifierName("initialRequest"), - SyntaxFactory.MemberBindingExpression(SyntaxFactory.IdentifierName("Limit")))), - SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - SyntaxFactory.IdentifierName("Cursor"), - SyntaxFactory.IdentifierName("cursor")), - ]))))); + + // The whole first request rides the continuation, so a filter added to the query needs no + // emitter change; only the first-page-only order is dropped and the cursor replaced. + statements.Add(SyntaxFactory.ReturnStatement(SyntaxFactory.WithExpression( + SyntaxFactory.ParenthesizedExpression(SyntaxFactory.BinaryExpression( + SyntaxKind.CoalesceExpression, + SyntaxFactory.IdentifierName("initialRequest"), + SyntaxFactory.ImplicitObjectCreationExpression())), + SyntaxFactory.InitializerExpression( + SyntaxKind.WithInitializerExpression, + SyntaxFactory.SeparatedList( + [ + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("Order"), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)), + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("Cursor"), + SyntaxFactory.IdentifierName("cursor")), + ]))))); return SyntaxFactory.MethodDeclaration(TypeSyntaxEmitter.EmitNamed(requestTypeName), "CreateNextRequest") .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( @@ -261,7 +265,8 @@ private static MethodDeclarationSyntax EmitCreateNextRequest(OperationPlan opera ]))) .WithBody(SyntaxFactory.Block(statements)) .WithLeadingTrivia(EmissionSyntax.Documentation( - "Creates a continuation request carrying the initial limit and opaque next cursor.")); + "Creates a continuation request carrying every initial filter beside the opaque next cursor, " + + "with the first-page-only order dropped.")); } /// From 4730af94b0701fd1c1fbdab5cda38050ca46e4a7 Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Sat, 12 Sep 2026 00:14:09 +0300 Subject: [PATCH 2/3] build: move SourceLink past CVE-2026-62900 Microsoft.Build.Tasks.Git 10.0.301, transitive through Microsoft.SourceLink.GitHub, carries GHSA-23fw-v26w-5fgq (published 2026-09-08). A cold restore reports NU1902 for it and the analyzer wall turns that into an error, so the next fresh CI restore would fail before compiling anything. 10.0.303 is the patched build; the package is build-only and never ships. --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7707cd15..c267013a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + From 9618ea0da5c4cbedf3855ff1b9daaeaa7c1c4ff1 Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Sat, 12 Sep 2026 00:32:56 +0300 Subject: [PATCH 3/3] feat: refresh the accepted snapshot to 20aff6d9 (beta-19425) The pin moves 94 commits to upstream's beta branch head, the commit that shipped as @opencode/cli@0.0.0-beta-19425, so a consumer can install the exact server this SDK is generated from. The README, the snapshot page, the roadmap, and ADR-0005 now name that scope and build, and the snapshot page says why the @opencode-ai/cli scope no longer reaches a server this SDK can drive: its next and latest tags froze at an August build. Upstream removed the message content mutation API (#48043), so v2.session.messageUpdate leaves the profile and the generated surface together with the assistant-content union only its body used, and session.message.content.updated leaves the live event union. The message list gains its type filter, which rides continuations unchanged now that cursor lists bind by inclusion. Provider and model records gain the websocket flag; compaction records carry their cost and token usage. Receipt: 139 operations, 355 components, restore patch 001 still applied, 21 watched anchors intact, only packages/server/src/process.ts moved by hash. Full gate: 5,177 tests green on the Windows matrix including the live lane. --- CHANGELOG.md | 20 ++ README.md | 16 +- docs/ROADMAP.md | 6 +- docs/adr/0005-v2-protocol-surface-only.md | 2 +- external/opencode | 2 +- spec/SNAPSHOT.md | 20 +- spec/openapi.json | 210 ++++++------------ spec/receipt.json | 20 +- spec/source-watch.json | 2 +- src/OpenCode.Sdk/.generated-manifest.json | 9 +- src/OpenCode.Sdk/.generation-incomplete | 3 +- ...essionMessageUpdatePatchResponseAdapter.cs | 66 ------ .../Serialization/EventJsonConverter.cs | 1 - .../Serialization/OpenCodeJsonContext.cs | 5 +- .../SessionMessageAssistantJsonConverter.cs | 48 ---- ...ssionMessageUpdatePatchResponseEnvelope.cs | 14 -- ...ownSessionMessageAssistantJsonConverter.cs | 48 ---- .../Models/ISessionMessageAssistant.cs | 18 -- .../Models/MessageListRequestType.cs | 63 ++++++ src/OpenCode.Sdk/Models/ModelInfo.cs | 7 + src/OpenCode.Sdk/Models/ProviderInfo.cs | 7 + .../Models/SessionCompactionEndedData.cs | 14 ++ .../Models/SessionCompactionFailedData.cs | 14 ++ .../SessionMessageAssistantReasoning.cs | 2 +- .../Models/SessionMessageAssistantText.cs | 2 +- .../Models/SessionMessageAssistantTool.cs | 2 +- .../SessionMessageCompactionCompleted.cs | 14 ++ .../Models/SessionMessageCompactionFailed.cs | 14 ++ .../Models/SessionMessageContentUpdated.cs | 2 +- .../SessionMessageUpdatePatchRequest.cs | 16 -- .../Models/UnknownSessionMessageAssistant.cs | 41 ---- src/OpenCode.Sdk/OpenCodeRoutes.cs | 46 ++-- .../Sessions/MessageListRequest.cs | 6 + src/OpenCode.Sdk/Sessions/SessionClient.cs | 19 +- .../SessionMessageUpdatePatchResponse.cs | 59 ----- .../Sessions/SessionClientContractTests.cs | 52 ----- .../Snapshots/PublicApi.verified.txt | 93 +++++--- .../Binding/OperationPlanBinderTests.cs | 1 - .../PendingOperationBindabilityProbeTests.cs | 1 - tools/generation-profile.txt | 1 - 40 files changed, 351 insertions(+), 635 deletions(-) delete mode 100644 src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageUpdatePatchResponseAdapter.cs delete mode 100644 src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantJsonConverter.cs delete mode 100644 src/OpenCode.Sdk/Internal/Serialization/SessionMessageUpdatePatchResponseEnvelope.cs delete mode 100644 src/OpenCode.Sdk/Internal/Serialization/UnknownSessionMessageAssistantJsonConverter.cs delete mode 100644 src/OpenCode.Sdk/Models/ISessionMessageAssistant.cs create mode 100644 src/OpenCode.Sdk/Models/MessageListRequestType.cs delete mode 100644 src/OpenCode.Sdk/Models/SessionMessageUpdatePatchRequest.cs delete mode 100644 src/OpenCode.Sdk/Models/UnknownSessionMessageAssistant.cs delete mode 100644 src/OpenCode.Sdk/Sessions/SessionMessageUpdatePatchResponse.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5992b240..0a9e7917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ Nightly builds of `master` are on [GitHub Packages](README.md#nightly-builds-github-packages) as `0.8.0-nightly.{yyyyMMdd}.{shortSha}`. +### 💥 Breaking changes + +- **The accepted snapshot moved to upstream `20aff6d9f643afe9abf8a048e68f019d049f5329`**, the + `beta` branch head that shipped as `@opencode/cli@0.0.0-beta-19425`. Install that exact build + (`npm install -g @opencode/cli@0.0.0-beta-19425`); the `@opencode-ai/cli` scope that the + previous README named is frozen at an August build that predates the worktree route shape and the + whole persistent-PTY family this SDK generates. +- **`v2.session.messageUpdate` is gone** because upstream removed the message content mutation API + ([anomalyco/opencode#48043](https://github.com/anomalyco/opencode/pull/48043)). + `SessionClient.PatchMessageUpdateAsync`, `SessionMessageUpdatePatchRequest`, + `SessionMessageUpdatePatchResponse`, and the route constants are removed, and with them the + `ISessionMessageAssistant` union and its `UnknownSessionMessageAssistant` carrier, which only that + request body referenced. Assistant content records keep `ISessionMessageAssistantContent`. +- **`session.message.content.updated` left the live event union.** `SessionMessageContentUpdated` + no longer implements `IEvent`; it remains a durable session-log item. + ### ✨ New features - **Session listing enumerates too.** `SessionsClient.EnumerateSessionsAsync` joins @@ -26,6 +42,10 @@ Nightly builds of `master` are on ### 🔧 Changes +- **Snapshot additions.** `MessageListRequest.Type` filters a message list by message type + (`MessageListRequestType`) and rides every continuation unchanged; `ModelInfo.Websocket` and + `ProviderInfo.Websocket` are new optional flags; compaction ended and failed data, and the + compaction message records, carry optional `Cost` and `Tokens`. - `SessionClient.EnumerateMessagesAsync` returns `CursorSequence` instead of `IAsyncEnumerable`. `await foreach` over it is unchanged, and code that stored the result in a variable typed `IAsyncEnumerable` still diff --git a/README.md b/README.md index 3067c38e..8323f196 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ hand-written transport runtime, so what you call is exactly what the server decl covered. What is still outstanding is opencode's third connection mode — attaching to a registered background service. -- ✅ **135 of 140 operations** callable, across 28 client families — sessions, PTYs, persistent +- ✅ **134 of 139 operations** callable, across 28 client families — sessions, PTYs, persistent PTYs, shells, events, MCP servers, integrations, providers, permissions, credentials, VCS, worktrees, websearch, RPC, and more -- ✅ **5,138 tests** green on Windows — the fullest leg, the only one that adds the `net472` +- ✅ **5,177 tests** green on Windows — the fullest leg, the only one that adds the `net472` assemblies. Linux and macOS run the same suite on `net8.0`, `net9.0`, and `net10.0` - ✅ **Server-sent event streams**, global and per-session, over the same transport as one-shot calls - ✅ **PTY and persistent-PTY terminal sessions** through hand-written WebSocket doors @@ -106,11 +106,15 @@ on, and the `net472` leg is what exercises its compile surface. ## Prerequisites -You need an `opencode` server. The v2 line ships as `@opencode-ai/cli@next` and installs a single -command, `opencode2` ([opencode.ai](https://opencode.ai)): +You need an `opencode` server. The v2 line ships from the `@opencode/cli` npm scope as `beta` +builds and installs a single command, `opencode2` ([opencode.ai](https://opencode.ai)). This SDK +is generated from the upstream commit that shipped as `0.0.0-beta-19425`, so install that exact +build; later `beta` builds usually work, but they are not what this repository tests. The pinned +commit and its build are owned by +[`spec/SNAPSHOT.md`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/spec/SNAPSHOT.md). ```sh -npm install -g @opencode-ai/cli@next +npm install -g @opencode/cli@0.0.0-beta-19425 ``` Then either run it yourself: @@ -273,7 +277,7 @@ overload shown above. ## 🧭 API Coverage -**135 of the 140 operations** in the pinned snapshot are callable. The remaining five are not +**134 of the 139 operations** in the pinned snapshot are callable. The remaining five are not omissions — each one is a recorded decision with a named cause, and [`src/OpenCode.Sdk/.generation-incomplete`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/src/OpenCode.Sdk/.generation-incomplete) is the machine-readable map that the build itself reads. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8074dbe4..ebe8c255 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # Roadmap -Date: 2026-09-08 +Date: 2026-09-12 Operational state: what ships today, what is queued next, what is still open, and what is known to be incomplete. This file is a summary and shrinks as work lands. `../AGENTS.md` routes to the @@ -15,7 +15,7 @@ accepted OpenAPI snapshot and rides one hand-written transport runtime. - **Protocol pin** — generation reads an accepted snapshot of upstream's `v2` OpenAPI document, never a live branch, and refreshes are receipt-governed (ADR-0020). `../spec/SNAPSHOT.md` owns the exact commit and the refresh procedure. -- **Coverage** — **135 of 140 operations selected** across 28 client families, with 3 declined by +- **Coverage** — **134 of 139 operations selected** across 28 client families, with 3 declined by decision and 2 transport-owned (Known Gaps below); `src/OpenCode.Sdk/.generation-incomplete` is the committed marker and names every one. One-shot calls, server-sent event streams (the global bus and the per-session log), PTY and persistent-PTY WebSocket sessions, cursor pagination, typed @@ -74,7 +74,7 @@ is revisited at each boundary. ## Open Questions -- **v2 GA watch** — the v2 line ships as `opencode2` (npm `@opencode-ai/cli@next`) with no GA date. +- **v2 GA watch** — the v2 line ships as `opencode2` (npm `@opencode/cli@beta`) with no GA date. The pin therefore stays a deliberate snapshot, refreshed under receipt at milestone boundaries. - **`v2.session.log` resume guarantees** — the pinned document exposes `after` as an optional string, and the generated surface stays faithful to it; ADR-0013 forbids importing the narrower diff --git a/docs/adr/0005-v2-protocol-surface-only.md b/docs/adr/0005-v2-protocol-surface-only.md index 9dca47e2..0373c5d4 100644 --- a/docs/adr/0005-v2-protocol-surface-only.md +++ b/docs/adr/0005-v2-protocol-surface-only.md @@ -27,7 +27,7 @@ internal research, 2026-08-08, "Upstream v1/v2: product version vs HTTP API surf consumer-driven legacy testing all disappear; milestone M5 shrinks to completing the generation profile over the single surface. - Until upstream's v2 line reaches general availability the SDK targets a pre-release - server (`opencode2`; npm `@opencode-ai/cli@next`) — accepted: the M-series timeline runs + server (`opencode2`; npm `@opencode/cli@beta`) — accepted: the M-series timeline runs alongside upstream's stabilization, and the pinned-snapshot + fail-closed refresh machinery exists for exactly this churn. - The spec pin is a snapshot of a moving branch: refreshes stay deliberate and diff --git a/external/opencode b/external/opencode index f9bc2233..20aff6d9 160000 --- a/external/opencode +++ b/external/opencode @@ -1 +1 @@ -Subproject commit f9bc2233ddcc11503a0804b317401bc221ef0e7f +Subproject commit 20aff6d9f643afe9abf8a048e68f019d049f5329 diff --git a/spec/SNAPSHOT.md b/spec/SNAPSHOT.md index e2fe0fd8..7680a795 100644 --- a/spec/SNAPSHOT.md +++ b/spec/SNAPSHOT.md @@ -1,6 +1,6 @@ # OpenAPI Snapshot -Date: 2026-09-08 +Date: 2026-09-11 `openapi.json` is the accepted snapshot of the upstream opencode OpenAPI 3.1 document — the v2 protocol surface (ADR-0005). The SDK is built against this snapshot, never against a live @@ -14,13 +14,17 @@ their hash-pinned manifests. |---|---| | Upstream file | `packages/protocol/openapi.json` | | Upstream branch | `v2` (active successor line; no release tags yet) | -| Commit | `f9bc2233ddcc11503a0804b317401bc221ef0e7f` | -| Upstream product channel | `opencode2` — npm `@opencode-ai/cli@next` (pre-release) | - -At this pin the CLI package is named `@opencode/cli`, following upstream's rename of its workspace -scope from `@opencode-ai`. The published channel has not moved with it: npm's `@opencode` scope -carries a reserved placeholder on `latest`, so `@opencode-ai/cli@next` stays the installable -pre-release and is what the row above names. +| Commit | `20aff6d9f643afe9abf8a048e68f019d049f5329` | +| Upstream product channel | `opencode2` — npm `@opencode/cli@beta` (pre-release); this commit shipped as `0.0.0-beta-19425` | + +Upstream publishes the v2 line from the `@opencode/cli` npm scope: every push to `v2` becomes a +`dev` build and every promotion of the `beta` branch becomes a `beta` build, versioned +`0.0.0--`. This pin is the `beta` branch head that shipped as +`@opencode/cli@0.0.0-beta-19425`, so `npm install -g @opencode/cli@0.0.0-beta-19425` installs a +server built from exactly this commit; later `beta` builds usually work but are not what this +repository tests. The former `@opencode-ai/cli` scope is frozen: its `next` and `latest` tags +stopped at `0.0.0-beta-17823` (published 2026-08-21) and the scope received no publish after +2026-09-07, so that install line no longer reaches a server this SDK can drive. `openapi.json` is derived from upstream's own generator output, so upstream's MIT notice travels with it in [`THIRD-PARTY-NOTICES.md`](../THIRD-PARTY-NOTICES.md). diff --git a/spec/openapi.json b/spec/openapi.json index 45a37daf..105ef6c3 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -4278,143 +4278,6 @@ }, "description": "Retrieve one projected message owned by the Session.", "summary": "Get session message" - }, - "patch": { - "tags": ["session"], - "operationId": "v2.session.messageUpdate", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Session.Message.Assistant" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | MessageNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/MessageNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "SessionBusyError | ConflictError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionBusyErrorEncoded" - }, - { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Replace the content of a completed assistant message in an idle session.", - "summary": "Update assistant message content", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "content": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.Assistant.Text" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" - }, - { - "$ref": "#/components/schemas/Session.Message.Assistant.Tool" - } - ] - } - } - }, - "required": ["content"], - "additionalProperties": false - } - } - }, - "required": true - } } }, "/api/session/{sessionID}/environment": { @@ -4627,6 +4490,34 @@ ] }, "required": false + }, + { + "name": "type", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": [ + "agent-switched", + "model-switched", + "location-switched", + "user", + "synthetic", + "system", + "skill", + "shell", + "assistant", + "compaction" + ] + }, + { + "type": "null" + } + ], + "description": "Filter by message type before pagination. When omitted, all message types are returned. Pass the same type when following cursors." + }, + "required": false } ], "security": [], @@ -4689,7 +4580,7 @@ } } }, - "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + "description": "Retrieve projected messages for a session, optionally filtered by type. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline, passing the same type filter on each page.", "summary": "Get session messages" } }, @@ -14380,6 +14271,9 @@ "compaction": { "$ref": "#/components/schemas/Provider.Compaction" }, + "websocket": { + "type": "boolean" + }, "modelID": { "type": "string" }, @@ -14488,6 +14382,9 @@ "compaction": { "$ref": "#/components/schemas/Provider.Compaction" }, + "websocket": { + "type": "boolean" + }, "canonical": { "type": "string" }, @@ -16472,6 +16369,9 @@ "compaction": { "$ref": "#/components/schemas/Provider.Compaction" }, + "websocket": { + "type": "boolean" + }, "settings": { "type": "object" }, @@ -17460,6 +17360,9 @@ "compaction": { "$ref": "#/components/schemas/Provider.Compaction" }, + "websocket": { + "type": "boolean" + }, "settings": { "type": "object" }, @@ -17892,10 +17795,10 @@ "$ref": "#/components/schemas/session.revert.committed" }, { - "$ref": "#/components/schemas/session.message.content.updated" + "$ref": "#/components/schemas/session.usage.recorded" }, { - "$ref": "#/components/schemas/session.usage.recorded" + "$ref": "#/components/schemas/session.message.content.updated" } ] }, @@ -18754,6 +18657,12 @@ }, "providerContext": { "$ref": "#/components/schemas/Session.ProviderContext" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": ["type", "id", "time", "status", "reason", "summary", "recent"], @@ -18793,6 +18702,12 @@ }, "error": { "$ref": "#/components/schemas/Session.StructuredError" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": ["type", "id", "time", "status", "reason", "error"], @@ -20414,9 +20329,6 @@ { "$ref": "#/components/schemas/session.revert.committed" }, - { - "$ref": "#/components/schemas/session.message.content.updated" - }, { "$ref": "#/components/schemas/filesystem.changed" }, @@ -22026,6 +21938,12 @@ }, "recent": { "type": "string" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": ["sessionID", "reason", "text", "recent"], @@ -22090,6 +22008,12 @@ "inputID": { "type": "string", "pattern": "^msg_" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": ["sessionID", "reason", "error"], diff --git a/spec/receipt.json b/spec/receipt.json index e3962659..2d623456 100644 --- a/spec/receipt.json +++ b/spec/receipt.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "upstreamCommit": "f9bc2233ddcc11503a0804b317401bc221ef0e7f", - "rawDocumentSha256": "0bef23a00ff19c711cd871c7f0f9cc592ec81552aa94344f701ae380d2d752dc", - "generatedBaselineSha256": "180707057921ff2e1103066b0993db3646b19afbddf000ff7dba74c0cd36f179", + "upstreamCommit": "20aff6d9f643afe9abf8a048e68f019d049f5329", + "rawDocumentSha256": "e2fc4e03e93547fd8f6a93a9dafcb497a4f6ae99b02f2eb6d5f3fa6eebac734c", + "generatedBaselineSha256": "e2fc4e03e93547fd8f6a93a9dafcb497a4f6ae99b02f2eb6d5f3fa6eebac734c", "patches": [ { "manifest": "001-restore-sse-payloads.json", @@ -14,17 +14,19 @@ }, { "path": "packages/protocol/src/groups/session.ts", - "sha256": "c4659072876fc3232650439cec9dd57d23b9c0b1666e3f8d1897cf1854b83047" + "sha256": "793e21081e6d9110e267f31549bf011fab68b3914026a546119d849b33528327" } ] } ], - "normalizedDocumentSha256": "809ad52800fa56ad266d2c5283d254c82200c64dae46b19a6edeba6e65bf41e2", + "normalizedDocumentSha256": "4a9513f7bde05861d627dad26ef94b7d8e789cdd7a5317bc471f52b9f8d0e92b", "normalizedDocumentPath": null, - "operationSetDigest": "038fa8d3b8cf12a9a3db9320f5945e5c7aaa5d07158cebc6a0527e4c56a0a2f8", - "operationCount": 140, + "operationSetDigest": "dd1a6ed926715137c7158bca3e7aaf9940aba556bd2d0fa0acad2260c5e8cb6b", + "operationCount": 139, "addedOperations": [], - "removedOperations": [], + "removedOperations": [ + "v2.session.messageUpdate" + ], "componentCount": 355, "contentSchemaCount": 2, "watchedSources": [ @@ -110,7 +112,7 @@ }, { "path": "packages/server/src/process.ts", - "sha256": "fb3bbb748814edfa92901e7228dfae707c3c668c4442e82648bf2fc6eaff8974", + "sha256": "88d89a520b684cba37499ad2f2f3f51f2cb60a4c397c550d09d70735b8514f42", "anchorMatched": true }, { diff --git a/spec/source-watch.json b/spec/source-watch.json index a2753d82..a58ab39e 100644 --- a/spec/source-watch.json +++ b/spec/source-watch.json @@ -147,7 +147,7 @@ }, { "path": "packages/server/src/process.ts", - "sha256": "fb3bbb748814edfa92901e7228dfae707c3c668c4442e82648bf2fc6eaff8974", + "sha256": "88d89a520b684cba37499ad2f2f3f51f2cb60a4c397c550d09d70735b8514f42", "behavior": "A ticketed PTY connect is admitted before the server reports ready.", "anchor": { "type": "contains", diff --git a/src/OpenCode.Sdk/.generated-manifest.json b/src/OpenCode.Sdk/.generated-manifest.json index e6614344..884fb44c 100644 --- a/src/OpenCode.Sdk/.generated-manifest.json +++ b/src/OpenCode.Sdk/.generated-manifest.json @@ -142,7 +142,6 @@ "Internal/ResponseAdapters/SessionInterruptPostResponseAdapter.cs", "Internal/ResponseAdapters/SessionListResponseAdapter.cs", "Internal/ResponseAdapters/SessionMessageResponseAdapter.cs", - "Internal/ResponseAdapters/SessionMessageUpdatePatchResponseAdapter.cs", "Internal/ResponseAdapters/SessionMovePostResponseAdapter.cs", "Internal/ResponseAdapters/SessionPermissionCreateResponseAdapter.cs", "Internal/ResponseAdapters/SessionPermissionListResponseAdapter.cs", @@ -264,12 +263,10 @@ "Internal/Serialization/SessionLogItemJsonConverter.cs", "Internal/Serialization/SessionMessageAssistantContentEncodedJsonConverter.cs", "Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs", - "Internal/Serialization/SessionMessageAssistantJsonConverter.cs", "Internal/Serialization/SessionMessageCompactionJsonConverter.cs", "Internal/Serialization/SessionMessageInfoJsonConverter.cs", "Internal/Serialization/SessionMessageResponseEnvelope.cs", "Internal/Serialization/SessionMessageToolStateJsonConverter.cs", - "Internal/Serialization/SessionMessageUpdatePatchResponseEnvelope.cs", "Internal/Serialization/SessionPermissionCreateResponseEnvelope.cs", "Internal/Serialization/SessionPermissionListResponseEnvelope.cs", "Internal/Serialization/SessionPermissionResponseEnvelope.cs", @@ -311,7 +308,6 @@ "Internal/Serialization/UnknownSessionLogItemJsonConverter.cs", "Internal/Serialization/UnknownSessionMessageAssistantContentEncodedJsonConverter.cs", "Internal/Serialization/UnknownSessionMessageAssistantContentJsonConverter.cs", - "Internal/Serialization/UnknownSessionMessageAssistantJsonConverter.cs", "Internal/Serialization/UnknownSessionMessageCompactionJsonConverter.cs", "Internal/Serialization/UnknownSessionMessageInfoJsonConverter.cs", "Internal/Serialization/UnknownSessionMessageToolStateJsonConverter.cs", @@ -429,7 +425,6 @@ "Models/ISessionInboxInfo.cs", "Models/ISessionInboxItem.cs", "Models/ISessionLogItem.cs", - "Models/ISessionMessageAssistant.cs", "Models/ISessionMessageAssistantContent.cs", "Models/ISessionMessageAssistantContentEncoded.cs", "Models/ISessionMessageCompaction.cs", @@ -504,6 +499,7 @@ "Models/McpStatusFailed.cs", "Models/McpStatusNeedsAuth.cs", "Models/McpStatusPending.cs", + "Models/MessageListRequestType.cs", "Models/MessageNotFoundError.cs", "Models/ModelCapabilities.cs", "Models/ModelCompatibility.cs", @@ -740,7 +736,6 @@ "Models/SessionMessageToolStateError.cs", "Models/SessionMessageToolStateRunning.cs", "Models/SessionMessageToolStateStreaming.cs", - "Models/SessionMessageUpdatePatchRequest.cs", "Models/SessionMessageUser.cs", "Models/SessionMessageUserTime.cs", "Models/SessionModelSelected.cs", @@ -924,7 +919,6 @@ "Models/UnknownSessionInboxInfo.cs", "Models/UnknownSessionInboxItem.cs", "Models/UnknownSessionLogItem.cs", - "Models/UnknownSessionMessageAssistant.cs", "Models/UnknownSessionMessageAssistantContent.cs", "Models/UnknownSessionMessageAssistantContentEncoded.cs", "Models/UnknownSessionMessageCompaction.cs", @@ -1053,7 +1047,6 @@ "Sessions/SessionListResponse.cs", "Sessions/SessionLogRequest.cs", "Sessions/SessionMessageResponse.cs", - "Sessions/SessionMessageUpdatePatchResponse.cs", "Sessions/SessionMovePostResponse.cs", "Sessions/SessionPermissionCreateResponse.cs", "Sessions/SessionPermissionListResponse.cs", diff --git a/src/OpenCode.Sdk/.generation-incomplete b/src/OpenCode.Sdk/.generation-incomplete index 87a3d16e..4d205fab 100644 --- a/src/OpenCode.Sdk/.generation-incomplete +++ b/src/OpenCode.Sdk/.generation-incomplete @@ -1,5 +1,5 @@ Generation is complete at the declared coverage; packages may be published. -Selected operations: 135 +Selected operations: 134 Pending operations: 0 Declined operations: 3 Transport-owned operations: 2 @@ -99,7 +99,6 @@ Selected: - v2.session.list - v2.session.log - v2.session.message -- v2.session.messageUpdate - v2.session.move - v2.session.permission.create - v2.session.permission.get diff --git a/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageUpdatePatchResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageUpdatePatchResponseAdapter.cs deleted file mode 100644 index 15f1ea16..00000000 --- a/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageUpdatePatchResponseAdapter.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using OpenCode.Sdk.Internal.Serialization; - -namespace OpenCode.Sdk.Internal.ResponseAdapters; -/// -/// Adapts the 'PATCH /api/session/{sessionID}/message/{messageID}' responses onto 'SessionMessageUpdatePatchResponse'. -/// -internal sealed class SessionMessageUpdatePatchResponseAdapter : ResponseAdapter -{ - private static readonly string[] Status400Tags = ["InvalidRequestError"]; - private static readonly string[] Status401Tags = ["UnauthorizedError"]; - private static readonly string[] Status404Tags = ["MessageNotFoundError", "SessionNotFoundError"]; - private static readonly string[] Status409Tags = ["ConflictError", "SessionBusyError"]; - private SessionMessageUpdatePatchResponseAdapter() - { - } - - /// - /// Gets the shared adapter instance. - /// - public static SessionMessageUpdatePatchResponseAdapter Instance { get; } = new SessionMessageUpdatePatchResponseAdapter(); - - /// - /// Classifies a status under this operation's pinned contract. - /// - public override StatusVerdict Classify(int status) => status switch - { - 200 => StatusVerdict.Success, - >= 200 and < 300 => StatusVerdict.UndeclaredSuccess, - 400 => StatusVerdict.DeclaredError, - 401 => StatusVerdict.DeclaredError, - 404 => StatusVerdict.DeclaredError, - 409 => StatusVerdict.DeclaredError, - _ => StatusVerdict.UndeclaredError - }; - /// - /// Maps the declared UTF-8 success body onto the typed envelope. - /// - public override SessionMessageUpdatePatchResponse AdaptSuccess(int status, ReadOnlySpan utf8Body) => new() - { - Status = status, - MessageUpdate = ReadBarePayload(utf8Body, OpenCodeJsonContext.Default.SessionMessageUpdatePatchResponseEnvelope).Data - }; - /// - /// Maps one buffered response onto the typed envelope. - /// - public override SessionMessageUpdatePatchResponse Adapt(int status, string rawBody) - { - ArgumentNullException.ThrowIfNull(rawBody); - return status switch - { - 200 => new SessionMessageUpdatePatchResponse - { - Status = status, - MessageUpdate = ReadBarePayload(rawBody, OpenCodeJsonContext.Default.SessionMessageUpdatePatchResponseEnvelope).Data - }, - >= 200 and < 300 => throw StatusVerdictFailures.UndeclaredSuccess(status), - 400 => new SessionMessageUpdatePatchResponse(status, ReadTolerantError(rawBody, Status400Tags), rawBody), - 401 => new SessionMessageUpdatePatchResponse(status, ReadTolerantError(rawBody, Status401Tags), rawBody), - 404 => new SessionMessageUpdatePatchResponse(status, ReadTolerantError(rawBody, Status404Tags), rawBody), - 409 => new SessionMessageUpdatePatchResponse(status, ReadTolerantError(rawBody, Status409Tags), rawBody), - _ => new SessionMessageUpdatePatchResponse(status, ReadTolerantError(rawBody, null), rawBody) - }; - } -} diff --git a/src/OpenCode.Sdk/Internal/Serialization/EventJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/EventJsonConverter.cs index 930a7755..ad67e065 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/EventJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/EventJsonConverter.cs @@ -57,7 +57,6 @@ internal sealed class EventJsonConverter : JsonConverter ["session.inbox.delivery.changed"] = typeof(SessionInboxDeliveryChanged), ["session.inbox.enqueued"] = typeof(SessionInboxEnqueued), ["session.instructions.updated"] = typeof(SessionInstructionsUpdated), - ["session.message.content.updated"] = typeof(SessionMessageContentUpdated), ["session.model.selected"] = typeof(SessionModelSelected), ["session.moved"] = typeof(SessionMoved), ["session.reasoning.delta"] = typeof(SessionReasoningDelta), diff --git a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs index 04a952df..3e262132 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs @@ -92,7 +92,6 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(ISessionInboxInfo))] [JsonSerializable(typeof(ISessionInboxItem))] [JsonSerializable(typeof(ISessionLogItem))] -[JsonSerializable(typeof(ISessionMessageAssistant))] [JsonSerializable(typeof(ISessionMessageAssistantContent))] [JsonSerializable(typeof(ISessionMessageAssistantContentEncoded))] [JsonSerializable(typeof(ISessionMessageCompaction))] @@ -175,6 +174,7 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(McpStatusFailed))] [JsonSerializable(typeof(McpStatusNeedsAuth))] [JsonSerializable(typeof(McpStatusPending))] +[JsonSerializable(typeof(MessageListRequestType))] [JsonSerializable(typeof(MessageListResponseEnvelope))] [JsonSerializable(typeof(MessageNotFoundError))] [JsonSerializable(typeof(ModelCapabilities))] @@ -451,8 +451,6 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(SessionMessageToolStateError))] [JsonSerializable(typeof(SessionMessageToolStateRunning))] [JsonSerializable(typeof(SessionMessageToolStateStreaming))] -[JsonSerializable(typeof(SessionMessageUpdatePatchRequest))] -[JsonSerializable(typeof(SessionMessageUpdatePatchResponseEnvelope))] [JsonSerializable(typeof(SessionMessageUser))] [JsonSerializable(typeof(SessionMessageUserTime))] [JsonSerializable(typeof(SessionModelSelected))] @@ -650,7 +648,6 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(UnknownSessionInboxInfo))] [JsonSerializable(typeof(UnknownSessionInboxItem))] [JsonSerializable(typeof(UnknownSessionLogItem))] -[JsonSerializable(typeof(UnknownSessionMessageAssistant))] [JsonSerializable(typeof(UnknownSessionMessageAssistantContent))] [JsonSerializable(typeof(UnknownSessionMessageAssistantContentEncoded))] [JsonSerializable(typeof(UnknownSessionMessageCompaction))] diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantJsonConverter.cs deleted file mode 100644 index 04670842..00000000 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantJsonConverter.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Collections.Frozen; -using System.Text.Json; -using System.Text.Json.Serialization; -using OpenCode.Sdk.Models; - -namespace OpenCode.Sdk.Internal.Serialization; - -internal sealed class SessionMessageAssistantJsonConverter : JsonConverter -{ - private static readonly FrozenDictionary TypesByTag = new Dictionary(StringComparer.Ordinal) - { - ["reasoning"] = typeof(SessionMessageAssistantReasoning), - ["text"] = typeof(SessionMessageAssistantText), - ["tool"] = typeof(SessionMessageAssistantTool) - }.ToFrozenDictionary(StringComparer.Ordinal); - private static readonly UnionDiscriminatorReader DiscriminatorReader = new(); - public override ISessionMessageAssistant Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - ArgumentNullException.ThrowIfNull(typeToConvert); - ArgumentNullException.ThrowIfNull(options); - if (DiscriminatorReader.TryFindKnown(ref reader, "type", "SessionMessageAssistant", TypesByTag, out var targetType, out var marker)) - { - var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for SessionMessageAssistant."); - return JsonSerializer.Deserialize(ref reader, typeInfo) as ISessionMessageAssistant ?? throw new JsonException("The SessionMessageAssistant payload deserialized to null."); - } - - using var document = JsonDocument.ParseValue(ref reader); - var payload = document.RootElement; - return new UnknownSessionMessageAssistant(marker, payload); - } - - public override void Write(Utf8JsonWriter writer, ISessionMessageAssistant value, JsonSerializerOptions options) - { - ArgumentNullException.ThrowIfNull(writer); - ArgumentNullException.ThrowIfNull(value); - ArgumentNullException.ThrowIfNull(options); - if (value is UnknownSessionMessageAssistant unknown) - { - unknown.Payload.WriteTo(writer); - return; - } - - var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for SessionMessageAssistant."); - JsonSerializer.Serialize(writer, value, typeInfo); - } -} diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageUpdatePatchResponseEnvelope.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageUpdatePatchResponseEnvelope.cs deleted file mode 100644 index 173d088c..00000000 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageUpdatePatchResponseEnvelope.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Text.Json.Serialization; -using OpenCode.Sdk.Models; - -namespace OpenCode.Sdk.Internal.Serialization; -/// -/// Carries the wire body of 'SessionMessageUpdatePatchResponse' through one deserialization pass. -/// -internal sealed record SessionMessageUpdatePatchResponseEnvelope -{ - [JsonPropertyName("data")] - public required SessionMessageAssistant Data { get; init; } -} diff --git a/src/OpenCode.Sdk/Internal/Serialization/UnknownSessionMessageAssistantJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/UnknownSessionMessageAssistantJsonConverter.cs deleted file mode 100644 index 0dd0f22f..00000000 --- a/src/OpenCode.Sdk/Internal/Serialization/UnknownSessionMessageAssistantJsonConverter.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Text.Json; -using System.Text.Json.Serialization; -using OpenCode.Sdk.Models; - -namespace OpenCode.Sdk.Internal.Serialization; - -internal sealed class UnknownSessionMessageAssistantJsonConverter : JsonConverter -{ - public override UnknownSessionMessageAssistant Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - ArgumentNullException.ThrowIfNull(typeToConvert); - ArgumentNullException.ThrowIfNull(options); - using var document = JsonDocument.ParseValue(ref reader); - var payload = document.RootElement; - if (payload.ValueKind != JsonValueKind.Object) - { - throw new JsonException("The SessionMessageAssistant payload must be a JSON object."); - } - - if (!payload.TryGetProperty("type", out var markerElement)) - { - throw new JsonException("The SessionMessageAssistant payload must contain 'type'."); - } - - if (markerElement.ValueKind != JsonValueKind.String) - { - throw new JsonException("The 'type' marker must be a string."); - } - - var marker = markerElement.GetString(); - if (marker is null || string.IsNullOrWhiteSpace(marker)) - { - throw new JsonException("The 'type' marker must be a non-empty string."); - } - - return new UnknownSessionMessageAssistant(marker, payload); - } - - public override void Write(Utf8JsonWriter writer, UnknownSessionMessageAssistant value, JsonSerializerOptions options) - { - ArgumentNullException.ThrowIfNull(writer); - ArgumentNullException.ThrowIfNull(value); - ArgumentNullException.ThrowIfNull(options); - value.Payload.WriteTo(writer); - } -} diff --git a/src/OpenCode.Sdk/Models/ISessionMessageAssistant.cs b/src/OpenCode.Sdk/Models/ISessionMessageAssistant.cs deleted file mode 100644 index 3ac83459..00000000 --- a/src/OpenCode.Sdk/Models/ISessionMessageAssistant.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Text.Json.Serialization; -using OpenCode.Sdk.Internal.Serialization; - -namespace OpenCode.Sdk.Models; -/// -/// Represents a session message assistant union. -/// -[JsonConverter(typeof(SessionMessageAssistantJsonConverter))] -public interface ISessionMessageAssistant -{ - /// - /// Gets the 'type' union marker. - /// - [JsonPropertyName("type")] - public string Type { get; } -} diff --git a/src/OpenCode.Sdk/Models/MessageListRequestType.cs b/src/OpenCode.Sdk/Models/MessageListRequestType.cs new file mode 100644 index 00000000..c5b23f43 --- /dev/null +++ b/src/OpenCode.Sdk/Models/MessageListRequestType.cs @@ -0,0 +1,63 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Defines the supported message list request type values. +/// +[JsonConverter(typeof(StrictJsonStringEnumConverter))] +public enum MessageListRequestType +{ + /// + /// Represents the 'agent-switched' wire value. + /// + [JsonStringEnumMemberName("agent-switched")] + AgentSwitched, + /// + /// Represents the 'model-switched' wire value. + /// + [JsonStringEnumMemberName("model-switched")] + ModelSwitched, + /// + /// Represents the 'location-switched' wire value. + /// + [JsonStringEnumMemberName("location-switched")] + LocationSwitched, + /// + /// Represents the 'user' wire value. + /// + [JsonStringEnumMemberName("user")] + User, + /// + /// Represents the 'synthetic' wire value. + /// + [JsonStringEnumMemberName("synthetic")] + Synthetic, + /// + /// Represents the 'system' wire value. + /// + [JsonStringEnumMemberName("system")] + System, + /// + /// Represents the 'skill' wire value. + /// + [JsonStringEnumMemberName("skill")] + Skill, + /// + /// Represents the 'shell' wire value. + /// + [JsonStringEnumMemberName("shell")] + Shell, + /// + /// Represents the 'assistant' wire value. + /// + [JsonStringEnumMemberName("assistant")] + Assistant, + /// + /// Represents the 'compaction' wire value. + /// + [JsonStringEnumMemberName("compaction")] + Compaction +} diff --git a/src/OpenCode.Sdk/Models/ModelInfo.cs b/src/OpenCode.Sdk/Models/ModelInfo.cs index 931b047e..fb9836b7 100644 --- a/src/OpenCode.Sdk/Models/ModelInfo.cs +++ b/src/OpenCode.Sdk/Models/ModelInfo.cs @@ -68,6 +68,13 @@ public sealed record ModelInfo [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IProviderCompaction? Compaction { get; init; } + /// + /// Gets the websocket value. + /// + [JsonPropertyName("websocket")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Websocket { get; init; } + /// /// Gets the settings value. /// diff --git a/src/OpenCode.Sdk/Models/ProviderInfo.cs b/src/OpenCode.Sdk/Models/ProviderInfo.cs index c93acef4..dc31ca5a 100644 --- a/src/OpenCode.Sdk/Models/ProviderInfo.cs +++ b/src/OpenCode.Sdk/Models/ProviderInfo.cs @@ -54,6 +54,13 @@ public sealed record ProviderInfo [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IProviderCompaction? Compaction { get; init; } + /// + /// Gets the websocket value. + /// + [JsonPropertyName("websocket")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Websocket { get; init; } + /// /// Gets the settings value. /// diff --git a/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs b/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs index 7b54b26d..6e1f3d61 100644 --- a/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs +++ b/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs @@ -53,4 +53,18 @@ public sealed record SessionCompactionEndedData /// [JsonPropertyName("recent")] public required string Recent { get; init; } + + /// + /// Gets the cost value. + /// + [JsonPropertyName("cost")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Cost { get; init; } + + /// + /// Gets the tokens value. + /// + [JsonPropertyName("tokens")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TokenUsageInfo? Tokens { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionCompactionFailedData.cs b/src/OpenCode.Sdk/Models/SessionCompactionFailedData.cs index 72c6d189..e283127b 100644 --- a/src/OpenCode.Sdk/Models/SessionCompactionFailedData.cs +++ b/src/OpenCode.Sdk/Models/SessionCompactionFailedData.cs @@ -32,4 +32,18 @@ public sealed record SessionCompactionFailedData [JsonPropertyName("inputID")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? InputId { get; init; } + + /// + /// Gets the cost value. + /// + [JsonPropertyName("cost")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Cost { get; init; } + + /// + /// Gets the tokens value. + /// + [JsonPropertyName("tokens")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TokenUsageInfo? Tokens { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs index b398f89d..cb0928ac 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs @@ -7,7 +7,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message assistant reasoning value. /// -public sealed record SessionMessageAssistantReasoning : ISessionMessageAssistantContent, ISessionMessageAssistantContentEncoded, ISessionMessageAssistant +public sealed record SessionMessageAssistantReasoning : ISessionMessageAssistantContent, ISessionMessageAssistantContentEncoded { /// /// Gets the type value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs index ed3b646b..49fba354 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs @@ -7,7 +7,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message assistant text value. /// -public sealed record SessionMessageAssistantText : ISessionMessageAssistantContent, ISessionMessageAssistantContentEncoded, ISessionMessageAssistant +public sealed record SessionMessageAssistantText : ISessionMessageAssistantContent, ISessionMessageAssistantContentEncoded { /// /// Gets the type value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs index 64e7c8e6..f70b6fbc 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs @@ -7,7 +7,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message assistant tool value. /// -public sealed record SessionMessageAssistantTool : ISessionMessageAssistantContent, ISessionMessageAssistantContentEncoded, ISessionMessageAssistant +public sealed record SessionMessageAssistantTool : ISessionMessageAssistantContent, ISessionMessageAssistantContentEncoded { /// /// Gets the type value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs index 4e236deb..2c6f13fe 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs @@ -78,4 +78,18 @@ public sealed record SessionMessageCompactionCompleted : ISessionMessageCompacti [JsonPropertyName("providerContext")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionProviderContext? ProviderContext { get; init; } + + /// + /// Gets the cost value. + /// + [JsonPropertyName("cost")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Cost { get; init; } + + /// + /// Gets the tokens value. + /// + [JsonPropertyName("tokens")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TokenUsageInfo? Tokens { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs index a59a93e5..892f39bd 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs @@ -51,4 +51,18 @@ public sealed record SessionMessageCompactionFailed : ISessionMessageCompaction /// [JsonPropertyName("error")] public required SessionStructuredError Error { get; init; } + + /// + /// Gets the cost value. + /// + [JsonPropertyName("cost")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Cost { get; init; } + + /// + /// Gets the tokens value. + /// + [JsonPropertyName("tokens")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TokenUsageInfo? Tokens { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageContentUpdated.cs b/src/OpenCode.Sdk/Models/SessionMessageContentUpdated.cs index fdfc0477..7f9953fc 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageContentUpdated.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageContentUpdated.cs @@ -7,7 +7,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message content updated value. /// -public sealed record SessionMessageContentUpdated : ISessionEventDurable, IEvent +public sealed record SessionMessageContentUpdated : ISessionEventDurable { /// /// Gets the id value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageUpdatePatchRequest.cs b/src/OpenCode.Sdk/Models/SessionMessageUpdatePatchRequest.cs deleted file mode 100644 index 62f2d1fc..00000000 --- a/src/OpenCode.Sdk/Models/SessionMessageUpdatePatchRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Text.Json.Serialization; - -namespace OpenCode.Sdk.Models; -/// -/// Represents a session message update patch request value. -/// -public sealed record SessionMessageUpdatePatchRequest -{ - /// - /// Gets the content value. - /// - [JsonPropertyName("content")] - public required IReadOnlyList Content { get; init; } -} diff --git a/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistant.cs b/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistant.cs deleted file mode 100644 index f3d5c228..00000000 --- a/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistant.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Text.Json; -using System.Text.Json.Serialization; -using OpenCode.Sdk.Internal.Serialization; - -namespace OpenCode.Sdk.Models; -/// -/// Preserves an unknown session message assistant payload. -/// -[JsonConverter(typeof(UnknownSessionMessageAssistantJsonConverter))] -public sealed record UnknownSessionMessageAssistant : ISessionMessageAssistant -{ - private readonly string _marker; - /// - /// Initializes an unknown union value from its marker and raw payload. - /// - [JsonConstructor] - public UnknownSessionMessageAssistant(string type, JsonElement payload) - { - ArgumentException.ThrowIfNullOrWhiteSpace(type); - if (payload.ValueKind is JsonValueKind.Undefined) - { - throw new ArgumentException("The payload must be a parsed JSON element.", nameof(payload)); - } - - UnionPayloadGuard.Instance.RequireString(payload, "type", type); - _marker = type; - Payload = payload.Clone(); - } - - /// - /// Gets the unrecognized 'type' marker. - /// - [JsonPropertyName("type")] - public string Type => _marker; - /// - /// Gets the preserved raw JSON payload. - /// - public JsonElement Payload { get; } -} diff --git a/src/OpenCode.Sdk/OpenCodeRoutes.cs b/src/OpenCode.Sdk/OpenCodeRoutes.cs index 6197525c..3529add0 100644 --- a/src/OpenCode.Sdk/OpenCodeRoutes.cs +++ b/src/OpenCode.Sdk/OpenCodeRoutes.cs @@ -2031,6 +2031,7 @@ public static string ListMessages(string sessionId, MessageListRequest? request query.AddText("limit", request.Limit); query.AddOrder("order", request.Order); query.AddText("cursor", request.Cursor); + query.AddText("type", ToWireValue(request.Type)); return path + query.Value; } @@ -2084,33 +2085,6 @@ public static string ListSessions(SessionListRequest? request = null) return path + query.Value; } - /// - /// The 'PATCH /api/session/{sessionID}/message/{messageID}' route template. - /// - public const string PatchMessageUpdateTemplate = "/api/session/{sessionID}/message/{messageID}"; - /// - /// Builds the '/api/session/{sessionID}/message/{messageID}' route. - /// - /// The 'sessionID' route value. - /// The 'messageID' route value. - /// The escaped route. - public static string PatchMessageUpdate(string sessionId, string messageId) - { - ArgumentException.ThrowIfNullOrWhiteSpace(sessionId); - if (sessionId is "." or "..") - { - throw new ArgumentException("Route values must not be dot segments.", nameof(sessionId)); - } - - ArgumentException.ThrowIfNullOrWhiteSpace(messageId); - if (messageId is "." or "..") - { - throw new ArgumentException("Route values must not be dot segments.", nameof(messageId)); - } - - return "/api/session/" + RouteValuePolicy.Escape(sessionId, nameof(sessionId)) + "/message/" + RouteValuePolicy.Escape(messageId, nameof(messageId)); - } - /// /// The 'POST /api/session/{sessionID}/background' route template. /// @@ -3232,6 +3206,24 @@ public static string RemoveWorktree(WorktreeRemoveRequest? request = null) _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown FsFindRequestType value.") }; /// + /// Spells one 'MessageListRequestType' member the way the wire query expects it. + /// + private static string? ToWireValue(MessageListRequestType? value) => value switch + { + MessageListRequestType.AgentSwitched => "agent-switched", + MessageListRequestType.ModelSwitched => "model-switched", + MessageListRequestType.LocationSwitched => "location-switched", + MessageListRequestType.User => "user", + MessageListRequestType.Synthetic => "synthetic", + MessageListRequestType.System => "system", + MessageListRequestType.Skill => "skill", + MessageListRequestType.Shell => "shell", + MessageListRequestType.Assistant => "assistant", + MessageListRequestType.Compaction => "compaction", + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown MessageListRequestType value.") + }; + /// /// Spells one 'SessionStatsRequestTools' member the way the wire query expects it. /// private static string? ToWireValue(SessionStatsRequestTools? value) => value switch diff --git a/src/OpenCode.Sdk/Sessions/MessageListRequest.cs b/src/OpenCode.Sdk/Sessions/MessageListRequest.cs index ceddefcb..05ff8923 100644 --- a/src/OpenCode.Sdk/Sessions/MessageListRequest.cs +++ b/src/OpenCode.Sdk/Sessions/MessageListRequest.cs @@ -1,9 +1,15 @@ // Generated by OpenCode.Sdk.Tools from spec/openapi.json. // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using OpenCode.Sdk.Models; + namespace OpenCode.Sdk; /// /// Shapes the 'GET /api/session/{sessionID}/message' query. 'limit': Maximum number of messages to return. When omitted, the endpoint returns its default page size. 'order': Message order for the first page. Use desc for newest first or asc for oldest first. 'cursor': Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order. /// public sealed record MessageListRequest : ListRequest { + /// + /// Filter by message type before pagination. When omitted, all message types are returned. Pass the same type when following cursors. + /// + public MessageListRequestType? Type { get; init; } } diff --git a/src/OpenCode.Sdk/Sessions/SessionClient.cs b/src/OpenCode.Sdk/Sessions/SessionClient.cs index c29a04dc..f6f29717 100644 --- a/src/OpenCode.Sdk/Sessions/SessionClient.cs +++ b/src/OpenCode.Sdk/Sessions/SessionClient.cs @@ -238,7 +238,7 @@ public virtual Task ListInstructionsEntryA } /// - /// Get session messages. Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline. + /// Get session messages. Retrieve projected messages for a session, optionally filtered by type. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline, passing the same type filter on each page. /// /// The request shaping the query. /// The per-call options. @@ -277,23 +277,6 @@ public virtual Task ListRequestsAsync(OpenCodeReq return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Sessions.ListRequests(SessionId), SessionPermissionListResponseAdapter.Instance, requestOptions, cancellationToken); } - /// - /// Update assistant message content. Replace the content of a completed assistant message in an idle session. - /// - /// The 'messageID' route value. - /// The request body. - /// The per-call options. - /// The cancellation token. - /// The 'SessionMessageUpdatePatchResponse' envelope. - /// The API returned an error status (declared: 400, 401, 404, 409) and NoThrow was not selected. - /// The server could not be reached or returned a malformed success body. - public virtual Task PatchMessageUpdateAsync(string messageId, SessionMessageUpdatePatchRequest request, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrEmpty(messageId); - ArgumentNullException.ThrowIfNull(request); - return Pipeline.ExecuteAsync(OpenCodeHttpMethod.Patch, OpenCodeRoutes.Sessions.PatchMessageUpdate(SessionId, messageId), request, OpenCodeJsonContext.Default.SessionMessageUpdatePatchRequest, SessionMessageUpdatePatchResponseAdapter.Instance, requestOptions, cancellationToken); - } - /// /// Background blocking session tools. Move active foreground backgroundable tools for this session into background observation. Idle requests are a no-op. /// diff --git a/src/OpenCode.Sdk/Sessions/SessionMessageUpdatePatchResponse.cs b/src/OpenCode.Sdk/Sessions/SessionMessageUpdatePatchResponse.cs deleted file mode 100644 index 6ecd85bb..00000000 --- a/src/OpenCode.Sdk/Sessions/SessionMessageUpdatePatchResponse.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Diagnostics.CodeAnalysis; -using System.Text; -using OpenCode.Sdk.Models; - -namespace OpenCode.Sdk; -/// -/// Represents the response of the 'PATCH /api/session/{sessionID}/message/{messageID}' operation. -/// -public sealed record SessionMessageUpdatePatchResponse : OpenCodeResponse -{ - private readonly SessionMessageAssistant? _messageUpdate; - /// - /// Initializes a success instance of the 'SessionMessageUpdatePatchResponse' envelope. - /// - public SessionMessageUpdatePatchResponse() - { - } - - /// - /// Initializes an error-path instance; the payload stays unset behind its guard. - /// - [SetsRequiredMembers] - internal SessionMessageUpdatePatchResponse(int status, IOpenCodeError? error, string? rawBody) - { - Status = status; - IsError = true; - Error = error; - RawBody = rawBody; - MessageUpdate = null!; - } - - /// - /// Gets the MessageUpdate payload; guarded on the error path. - /// - public required SessionMessageAssistant MessageUpdate { get => _messageUpdate ?? throw new InvalidOperationException("The response is an error; check IsError before accessing MessageUpdate."); init => _messageUpdate = value; } - - /// - /// Prints the shared metadata and appends the payload only when it is present. - /// - protected override bool PrintMembers(StringBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - var printed = base.PrintMembers(builder); - if (_messageUpdate is null) - { - return printed; - } - - if (printed) - { - _ = builder.Append(", "); - } - - _ = builder.Append("MessageUpdate = ").Append(_messageUpdate); - return true; - } -} diff --git a/tests/OpenCode.Sdk.Tests/Sessions/SessionClientContractTests.cs b/tests/OpenCode.Sdk.Tests/Sessions/SessionClientContractTests.cs index dd811fbb..35255ab2 100644 --- a/tests/OpenCode.Sdk.Tests/Sessions/SessionClientContractTests.cs +++ b/tests/OpenCode.Sdk.Tests/Sessions/SessionClientContractTests.cs @@ -1019,58 +1019,6 @@ public async Task PostFormReplyAsync_Should_Return_The_401_Error_On_The_NoThrow_ await Assert.That(response.Error).IsTypeOf(); } - [Test] - public async Task PatchMessageUpdateAsync_Should_Send_The_Tagged_Content_And_Return_The_Typed_Message() - { - const string message = "{\"id\":\"msg_1\",\"time\":{\"created\":1},\"agent\":\"build\"," - + "\"model\":{\"id\":\"gpt-4\",\"providerID\":\"openai\"},\"content\":[{\"type\":\"text\",\"text\":\"hello\"}]}"; - using var scenario = ContractScenario.Responding(HttpStatusCode.OK, WireBodyData.Envelope(message)); - - var response = await scenario.Client.Sessions.GetSessionClient("ses_100").PatchMessageUpdateAsync("msg_1", new SessionMessageUpdatePatchRequest - { - Content = [new SessionMessageAssistantText { Text = "hello", }], - }); - - await Assert.That(response.MessageUpdate.Id).IsEqualTo("msg_1"); - await Assert.That(response.MessageUpdate.Content.Single()).IsTypeOf(); - var request = scenario.Requests.Single(); - await Assert.That(request.Method.Method).IsEqualTo("PATCH"); - await Assert.That(request.RequestUri) - .IsEqualTo(new Uri("http://localhost:4096/api/session/ses_100/message/msg_1")); - await Assert.That(request.Body).IsEqualTo("{\"content\":[{\"type\":\"text\",\"text\":\"hello\"}]}"); - } - - [Test] - public async Task PatchMessageUpdateAsync_Should_Throw_The_Declared_404_Message_Not_Found() - { - using var scenario = ContractScenario.Responding(HttpStatusCode.NotFound, WireBodyData.MessageNotFoundError); - - var exception = await Assert - .That(async () => _ = await scenario.Client.Sessions.GetSessionClient("ses_100").PatchMessageUpdateAsync("msg_9", new SessionMessageUpdatePatchRequest - { - Content = [new SessionMessageAssistantText { Text = "hello", }], - })) - .Throws(); - - await Assert.That(exception!.Status).IsEqualTo(404); - await Assert.That(exception.Error).IsTypeOf(); - } - - [Test] - public async Task PatchMessageUpdateAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine() - { - using var scenario = ContractScenario.Responding(HttpStatusCode.Unauthorized, WireBodyData.UnauthorizedError); - - var response = await scenario.Client.Sessions.GetSessionClient("ses_100").PatchMessageUpdateAsync( - "msg_1", - new SessionMessageUpdatePatchRequest { Content = [], }, - OpenCodeRequestOptions.NoThrow); - - await Assert.That(response.IsError).IsTrue(); - await Assert.That(response.Status).IsEqualTo(401); - await Assert.That(response.Error).IsTypeOf(); - } - [Test] public async Task PostViewAsync_Should_Send_The_Typed_Body_On_The_204() { diff --git a/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt index 57b89192..e2c3c5ad 100644 --- a/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt +++ b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt @@ -420,6 +420,7 @@ public sealed class MessageListRequest : OpenCode.Sdk.ListRequest, System.IEquatable { public MessageListRequest() { } + public OpenCode.Sdk.Models.MessageListRequestType? Type { get; init; } } public sealed class MessageListResponse : OpenCode.Sdk.OpenCodeResponse, System.IEquatable { @@ -743,7 +744,6 @@ public const string ListMessagesTemplate = "/api/session/{sessionID}/message"; public const string ListRequestsTemplate = "/api/session/{sessionID}/permission"; public const string ListSessionsTemplate = "/api/session"; - public const string PatchMessageUpdateTemplate = "/api/session/{sessionID}/message/{messageID}"; public const string PostBackgroundTemplate = "/api/session/{sessionID}/background"; public const string PostCommandTemplate = "/api/session/{sessionID}/command"; public const string PostCompactTemplate = "/api/session/{sessionID}/compact"; @@ -791,7 +791,6 @@ public static string ListMessages(string sessionId, OpenCode.Sdk.MessageListRequest? request = null) { } public static string ListRequests(string sessionId) { } public static string ListSessions(OpenCode.Sdk.SessionListRequest? request = null) { } - public static string PatchMessageUpdate(string sessionId, string messageId) { } public static string PostBackground(string sessionId) { } public static string PostCommand(string sessionId) { } public static string PostCompact(string sessionId) { } @@ -1415,7 +1414,6 @@ public virtual System.Threading.Tasks.Task ListInstructionsEntryAsync(OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task ListMessagesAsync(OpenCode.Sdk.MessageListRequest? request = null, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task ListRequestsAsync(OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } - public virtual System.Threading.Tasks.Task PatchMessageUpdateAsync(string messageId, OpenCode.Sdk.Models.SessionMessageUpdatePatchRequest request, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task PostBackgroundAsync(OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task PostCommandAsync(OpenCode.Sdk.Models.SessionCommandPostRequest request, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } public virtual System.Threading.Tasks.Task PostCompactAsync(OpenCode.Sdk.Models.SessionCompactPostRequest? request = null, OpenCode.Sdk.OpenCodeRequestOptions? requestOptions = null, System.Threading.CancellationToken cancellationToken = default) { } @@ -1604,12 +1602,6 @@ public required OpenCode.Sdk.Models.ISessionMessageInfo Message { get; init; } protected override bool PrintMembers(System.Text.StringBuilder builder) { } } - public sealed class SessionMessageUpdatePatchResponse : OpenCode.Sdk.OpenCodeResponse, System.IEquatable - { - public SessionMessageUpdatePatchResponse() { } - public required OpenCode.Sdk.Models.SessionMessageAssistant MessageUpdate { get; init; } - protected override bool PrintMembers(System.Text.StringBuilder builder) { } - } public sealed class SessionMovePostResponse : OpenCode.Sdk.OpenCodeResponse, System.IEquatable { public SessionMovePostResponse() { } @@ -2905,12 +2897,6 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("type")] string Type { get; } } - [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.SessionMessageAssistantJsonConverter))] - public interface ISessionMessageAssistant - { - [System.Text.Json.Serialization.JsonPropertyName("type")] - string Type { get; } - } [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.SessionMessageAssistantContentJsonConverter))] public interface ISessionMessageAssistantContent { @@ -3679,6 +3665,30 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("status")] public string Status { get; } } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.StrictJsonStringEnumConverter))] + public enum MessageListRequestType + { + [System.Text.Json.Serialization.JsonStringEnumMemberName("agent-switched")] + AgentSwitched = 0, + [System.Text.Json.Serialization.JsonStringEnumMemberName("model-switched")] + ModelSwitched = 1, + [System.Text.Json.Serialization.JsonStringEnumMemberName("location-switched")] + LocationSwitched = 2, + [System.Text.Json.Serialization.JsonStringEnumMemberName("user")] + User = 3, + [System.Text.Json.Serialization.JsonStringEnumMemberName("synthetic")] + Synthetic = 4, + [System.Text.Json.Serialization.JsonStringEnumMemberName("system")] + System = 5, + [System.Text.Json.Serialization.JsonStringEnumMemberName("skill")] + Skill = 6, + [System.Text.Json.Serialization.JsonStringEnumMemberName("shell")] + Shell = 7, + [System.Text.Json.Serialization.JsonStringEnumMemberName("assistant")] + Assistant = 8, + [System.Text.Json.Serialization.JsonStringEnumMemberName("compaction")] + Compaction = 9, + } public sealed class MessageNotFoundError : OpenCode.Sdk.Models.IOpenCodeError, System.IEquatable { public MessageNotFoundError() { } @@ -3801,6 +3811,9 @@ namespace OpenCode.Sdk.Models public required OpenCode.Sdk.Models.ModelInfoTime Time { get; init; } [System.Text.Json.Serialization.JsonPropertyName("variants")] public required System.Collections.Generic.IReadOnlyList Variants { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("websocket")] + public bool? Websocket { get; init; } } public sealed class ModelInfoLimit : System.IEquatable { @@ -4637,6 +4650,9 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] [System.Text.Json.Serialization.JsonPropertyName("settings")] public System.Collections.Generic.IReadOnlyDictionary? Settings { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("websocket")] + public bool? Websocket { get; init; } } [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.StrictJsonStringEnumConverter))] public enum ProviderInfoActivation @@ -5127,6 +5143,9 @@ namespace OpenCode.Sdk.Models { public SessionCompactionEndedData() { } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("cost")] + public double? Cost { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] [System.Text.Json.Serialization.JsonPropertyName("model")] public OpenCode.Sdk.Models.ModelRef? Model { get; init; } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] @@ -5143,6 +5162,9 @@ namespace OpenCode.Sdk.Models public required string SessionId { get; init; } [System.Text.Json.Serialization.JsonPropertyName("text")] public required string Text { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("tokens")] + public OpenCode.Sdk.Models.TokenUsageInfo? Tokens { get; init; } } [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.StrictJsonStringEnumConverter))] public enum SessionCompactionEndedDataReason @@ -5185,6 +5207,9 @@ namespace OpenCode.Sdk.Models public sealed class SessionCompactionFailedData : System.IEquatable { public SessionCompactionFailedData() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("cost")] + public double? Cost { get; init; } [System.Text.Json.Serialization.JsonPropertyName("error")] public required OpenCode.Sdk.Models.SessionStructuredError Error { get; init; } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] @@ -5194,6 +5219,9 @@ namespace OpenCode.Sdk.Models public required OpenCode.Sdk.Models.SessionCompactionFailedDataReason Reason { get; init; } [System.Text.Json.Serialization.JsonPropertyName("sessionID")] public required string SessionId { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("tokens")] + public OpenCode.Sdk.Models.TokenUsageInfo? Tokens { get; init; } } [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.StrictJsonStringEnumConverter))] public enum SessionCompactionFailedDataReason @@ -6222,7 +6250,7 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonStringEnumMemberName("unknown")] Unknown = 5, } - public sealed class SessionMessageAssistantReasoning : OpenCode.Sdk.Models.ISessionMessageAssistant, OpenCode.Sdk.Models.ISessionMessageAssistantContent, OpenCode.Sdk.Models.ISessionMessageAssistantContentEncoded, System.IEquatable + public sealed class SessionMessageAssistantReasoning : OpenCode.Sdk.Models.ISessionMessageAssistantContent, OpenCode.Sdk.Models.ISessionMessageAssistantContentEncoded, System.IEquatable { public SessionMessageAssistantReasoning() { } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] @@ -6268,7 +6296,7 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("start")] public string? Start { get; init; } } - public sealed class SessionMessageAssistantText : OpenCode.Sdk.Models.ISessionMessageAssistant, OpenCode.Sdk.Models.ISessionMessageAssistantContent, OpenCode.Sdk.Models.ISessionMessageAssistantContentEncoded, System.IEquatable + public sealed class SessionMessageAssistantText : OpenCode.Sdk.Models.ISessionMessageAssistantContent, OpenCode.Sdk.Models.ISessionMessageAssistantContentEncoded, System.IEquatable { public SessionMessageAssistantText() { } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] @@ -6291,7 +6319,7 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("streamed")] public double? Streamed { get; init; } } - public sealed class SessionMessageAssistantTool : OpenCode.Sdk.Models.ISessionMessageAssistant, OpenCode.Sdk.Models.ISessionMessageAssistantContent, OpenCode.Sdk.Models.ISessionMessageAssistantContentEncoded, System.IEquatable + public sealed class SessionMessageAssistantTool : OpenCode.Sdk.Models.ISessionMessageAssistantContent, OpenCode.Sdk.Models.ISessionMessageAssistantContentEncoded, System.IEquatable { public SessionMessageAssistantTool() { } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] @@ -6329,6 +6357,9 @@ namespace OpenCode.Sdk.Models public sealed class SessionMessageCompactionCompleted : OpenCode.Sdk.Models.ISessionMessageCompaction, OpenCode.Sdk.Models.ISessionMessageInfo, System.IEquatable { public SessionMessageCompactionCompleted() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("cost")] + public double? Cost { get; init; } [System.Text.Json.Serialization.JsonPropertyName("id")] public required string Id { get; init; } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] @@ -6353,6 +6384,9 @@ namespace OpenCode.Sdk.Models public required string Summary { get; init; } [System.Text.Json.Serialization.JsonPropertyName("time")] public required OpenCode.Sdk.Models.SessionMessageCompactionCompletedTime Time { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("tokens")] + public OpenCode.Sdk.Models.TokenUsageInfo? Tokens { get; init; } [System.Text.Json.Serialization.JsonPropertyName("type")] public string Type { get; } } @@ -6373,6 +6407,9 @@ namespace OpenCode.Sdk.Models public sealed class SessionMessageCompactionFailed : OpenCode.Sdk.Models.ISessionMessageCompaction, OpenCode.Sdk.Models.ISessionMessageInfo, System.IEquatable { public SessionMessageCompactionFailed() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("cost")] + public double? Cost { get; init; } [System.Text.Json.Serialization.JsonPropertyName("error")] public required OpenCode.Sdk.Models.SessionStructuredError Error { get; init; } [System.Text.Json.Serialization.JsonPropertyName("id")] @@ -6386,6 +6423,9 @@ namespace OpenCode.Sdk.Models public string Status { get; } [System.Text.Json.Serialization.JsonPropertyName("time")] public required OpenCode.Sdk.Models.SessionMessageCompactionFailedTime Time { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("tokens")] + public OpenCode.Sdk.Models.TokenUsageInfo? Tokens { get; init; } [System.Text.Json.Serialization.JsonPropertyName("type")] public string Type { get; } } @@ -6438,7 +6478,7 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("created")] public required double Created { get; init; } } - public sealed class SessionMessageContentUpdated : OpenCode.Sdk.Models.IEvent, OpenCode.Sdk.Models.ISessionEventDurable, OpenCode.Sdk.Models.ISessionLogItem, System.IEquatable + public sealed class SessionMessageContentUpdated : OpenCode.Sdk.Models.ISessionEventDurable, OpenCode.Sdk.Models.ISessionLogItem, System.IEquatable { public SessionMessageContentUpdated() { } [System.Text.Json.Serialization.JsonPropertyName("created")] @@ -6723,12 +6763,6 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("status")] public string Status { get; } } - public sealed class SessionMessageUpdatePatchRequest : System.IEquatable - { - public SessionMessageUpdatePatchRequest() { } - [System.Text.Json.Serialization.JsonPropertyName("content")] - public required System.Collections.Generic.IReadOnlyList Content { get; init; } - } public sealed class SessionMessageUser : OpenCode.Sdk.Models.ISessionMessageInfo, System.IEquatable { public SessionMessageUser() { } @@ -9097,15 +9131,6 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("type")] public string Type { get; } } - [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.UnknownSessionMessageAssistantJsonConverter))] - public sealed class UnknownSessionMessageAssistant : OpenCode.Sdk.Models.ISessionMessageAssistant, System.IEquatable - { - [System.Text.Json.Serialization.JsonConstructor] - public UnknownSessionMessageAssistant(string type, System.Text.Json.JsonElement payload) { } - public System.Text.Json.JsonElement Payload { get; } - [System.Text.Json.Serialization.JsonPropertyName("type")] - public string Type { get; } - } [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.UnknownSessionMessageAssistantContentJsonConverter))] public sealed class UnknownSessionMessageAssistantContent : OpenCode.Sdk.Models.ISessionMessageAssistantContent, System.IEquatable { diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs index dbb69005..b5d63a9b 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs @@ -331,7 +331,6 @@ await Assert "ListInstructionsEntryAsync", "ListMessagesAsync", "ListRequestsAsync", - "PatchMessageUpdateAsync", "PostBackgroundAsync", "PostCommandAsync", "PostCompactAsync", diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs index c142d26b..e733c1a7 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs @@ -118,7 +118,6 @@ await Assert.That(marks.Select(static mark => mark.OperationId) [Arguments("v2.session.form.get")] [Arguments("v2.session.form.reply")] [Arguments("v2.session.form.state")] - [Arguments("v2.session.messageUpdate")] [Arguments("v2.session.view")] [Arguments("v2.workspace.destroy")] public async Task Probe_Should_Mark_Every_Known_Wall_Free_Pending_Operation_As_Bindable(string operationId) diff --git a/tools/generation-profile.txt b/tools/generation-profile.txt index bbd2a15c..a16b0376 100644 --- a/tools/generation-profile.txt +++ b/tools/generation-profile.txt @@ -93,7 +93,6 @@ v2.session.interrupt v2.session.list v2.session.log v2.session.message -v2.session.messageUpdate v2.session.move v2.session.permission.create v2.session.permission.get