Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,56 @@ 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}`.

### 💥 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
`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<TPage, TItem>`, 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

- **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<MessageListResponse,
ISessionMessageInfo>` instead of `IAsyncEnumerable<ISessionMessageInfo>`. `await foreach` over it
is unchanged, and code that stored the result in a variable typed `IAsyncEnumerable<T>` still
compiles, because `CursorSequence<TPage, TItem>` implements `IAsyncEnumerable<TItem>`. Code that
declared the variable with `var` and then assigned an `IAsyncEnumerable<T>` 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
Expand Down
5 changes: 5 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TPage, TItem>`):
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
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.10" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.303" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.11" />
<PackageVersion Include="System.Text.Json" Version="10.0.11" />
<!-- downlevel targets (netstandard2.0 / net472) -->
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -305,7 +309,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.
Expand Down
6 changes: 3 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0005-v2-protocol-surface-only.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 20 additions & 14 deletions docs/adr/0017-cursor-pagination-keeps-explicit-pages.md
Original file line number Diff line number Diff line change
@@ -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<TItem>`. 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<T>`/`Page<T>` 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<TPage, TItem>`: a lazy `IAsyncEnumerable<TItem>` 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<T>`/`Page<T>` 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).
25 changes: 16 additions & 9 deletions docs/architecture/client-runtime.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<TPage, TItem>` 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<TPage, TItem>` 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
Expand Down
11 changes: 8 additions & 3 deletions docs/architecture/protocol-and-generation.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading