diff --git a/docs/opencode-api-2-assessment.md b/docs/opencode-api-2-assessment.md index b2d60c48..d04fe3ac 100644 --- a/docs/opencode-api-2-assessment.md +++ b/docs/opencode-api-2-assessment.md @@ -72,6 +72,26 @@ There is no version string "2.0" in the running server. `GET /doc` self-reports So "API 2.0" most likely means **the `/api/*` surface — which we already run on for turns**. Its operation ids are literally `v2.*`. It is not something to adopt; it is something to *finish*. +Two qualifications, both from the source at tag `v1.18.16`: + +- **Upstream calls it experimental.** `packages/protocol/src/api.ts` titles it `"opencode HttpApi"`, + version `"0.0.1"`, described as *"Experimental HttpApi surface for selected instance routes"*, with + every group annotated the same way. Meanwhile `/session/*` is the surface the public docs actually + document, and it is not deprecated. The internal direction is unambiguous; the external commitment is + nil. +- **`session.next` is the event family of that rewritten engine, and the name is already dead + upstream.** It arrived in **1.15.0** (PR #27415, "Add Effect-native core event system", merged + 2026-05-15) as an interim prefix. On the `v2` branch all 36 session events have dropped `.next.` — + `session.step.started`, `session.text.delta` — along with renames: `agent.switched` → + `agent.selected`, `model.switched` → `model.selected`, `prompted` → `prompt.promoted`. Those renames + are **v2-branch only**; the 1.x line we run still emits `session.next.*`. Code against + `session.next.*` today, but put the names behind one mapping table, because they are scheduled to + change wholesale. + +Same for the `v2` suffix itself. `packages/schema/AGENTS.md`: *"V1 coexistence is temporary… delete the +V1 subtree when the legacy runtime is retired"* and *"Do not preserve `V2` as the permanent name for the +replacement architecture."* Both halves of today's naming are transitional. + **OpenCode 2.0 the product is a different question**, and the answer tonight is not yet: the beta docs carry the banner *"we may wipe your data, things may break, and APIs, configuration, and plugin APIs may change"*, releases ship ~6/day, and the migration guide states three intentional breaking changes @@ -162,6 +182,21 @@ for more. Every durable event carries `{aggregateID, seq: integer, version}`, so integer. This is the documented, working answer to the gap Phase B left open and `docs/opencode-testing-checklist.md` calls the most likely thing to be broken. +The upstream implementation (`packages/core/src/event.ts`, `durable()`) makes three things explicit +that matter for building on it: + +- `after` is an **exclusive** lower bound on the durable seq, and the aggregate is the session. + Omitting it replays the session from 0. +- **Replay-then-live is gap-free by construction**: it reads `WHERE seq > after ORDER BY seq ASC`, + advances its cursor to the last row, and on every wake re-reads *the database* rather than draining a + pubsub buffer. Sequences are strictly monotonic and contiguous per session, enforced with explicit + `Sequence mismatch` / `Replay diverged` errors. +- **The first cursor is free.** `POST …/prompt` returns `{admittedSeq, id, sessionID, prompt, delivery, + timeCreated, promotedSeq?}` — measured at 22 ms — and `admittedSeq` feeds straight back as `after`. + +Note the two cursor kinds are unrelated: the session *list* uses an opaque base64url cursor +(`cursor.previous` / `cursor.next`), this one is a plain integer. + **But the two streams are not interchangeable, and the schema says why.** `SessionDurableEvent` is a `oneOf` of exactly 28 members, and the five it omits are `text.delta`, `tool.input.delta`, `reasoning.delta`, `compaction.delta` and the retry error. **Deltas are live-only by design; the @@ -192,6 +227,23 @@ GET /api/session/{id}/question POST …/question/{requestID}/r Plus `permission.v2.asked` / `question.v2.asked` events (the v1 families still exist alongside; the only `deprecated: true` operation in the entire document is `POST /session/{id}/permissions/{id}`). +The two "v2"s are not the same kind of change, which matters if we implement one of them: + +- **Permissions v2 is a real contract change.** A rule goes from `{permission, pattern, action}` to + `{action, resource, effect}`; a request from `{permission, patterns[], metadata, always[], tool?}` to + `{action, resources[], save?[], metadata?, source?}`, with the tool linkage becoming a tagged union + `source: {type:"tool", messageID, callID}`; and the reply loses its free-text `message`. The public + V2 docs say the same in config terms: *"Do not use `permission`, `bash`, or `task` in V2 + configuration."* +- **Questions v2 is a re-homing.** Field shapes are byte-identical to v1 — `questions[]` of + `{question, header, options[], multiple?, custom?}`, answers as `string[][]`. Only the namespace and + event names changed. + +Which family a 1.18.16 agent actually emits is worth measuring before building UI: the manifest the +`/api` protocol is *built* from excludes the v1 families, but the server wires the **full** manifest +(`makeApi({definitions: EventManifest.Latest.values()})`), which is why both appear in the `/api/event` +union on our own `/doc`. + An opencode agent that wants consent, or that asks a question mid-turn, gets no answer from officer. We don't subscribe to those events and have no route to reply on. Claude's harness runs `--dangerously-skip-permissions`, so this has never been modelled for either harness. Largest single @@ -271,9 +323,8 @@ with no model, against a serve with no configured default, hangs silently.** Wor comments, no `retry:`, no multi-line frames, fixed 1 s reconnect with no backoff. A cursored stream must resume at `?after=`, not restart. - **Two envelope unwrappers and three hand-written type sets** (`serve-runner.ts:161`, `client.ts:38`; - types pinned by comment to two different opencode versions, 1.17.9 and 1.18.16). `@opencode-ai/sdk/v2` - would replace most of this — a dependency decision, and note our install is frozen, so it is a - deliberate lockfile change. + types pinned by comment to two different opencode versions, 1.17.9 and 1.18.16). This is the part a + dependency would delete outright — see below. - **Stale comments in at least nine files** still describe the deleted `opencode run` subprocess path (`protocol.ts:198`, `serve-events.ts:5`, `connect-credential.ts:24`, `index.ts:137`, `websocket.ts:454`, `chat.ts:127`, `list-models.ts:75`, `sidecar-server.ts:8`, `send-opencode.ts:7`). @@ -286,6 +337,40 @@ Unrelated but found while inventorying: the settings UI writes provider keys to --- +## 7b. The SDK is generated from the document we have been reading by hand + +`@opencode-ai/sdk@1.18.16` (published 2026-08-10, versioned in lockstep with the CLI) is built by +`packages/sdk/js/script/build.ts`, which runs opencode's own `generate` to produce the OpenAPI document +and feeds it to `@hey-api/openapi-ts`. **It is generated from the same `/doc` we probed**, which is +about as good a guarantee of shape-agreement as exists. + +It ships two clients. The default export is the legacy surface. `@opencode-ai/sdk/v2` is ours: + +```ts +import { createOpencodeClient } from '@opencode-ai/sdk/v2'; + +const client = createOpencodeClient({ baseUrl }); +const admitted = await client.v2.session.prompt({ sessionID, prompt: { text }, delivery: 'steer' }); +const events = await client.v2.session.events({ sessionID, after: admitted.data.admittedSeq }); +for await (const ev of events.stream) { /* ev.type, ev.durable.seq */ } +``` + +`client.v2.session.*` covers list/create/active/get/switchAgent/switchModel/prompt/compact/wait/ +context/history/events/interrupt/message(s); there is also `client.v2.event.subscribe`, +`client.v2.permission.*`, `client.v2.question.*`, `fs`, `model`, `provider`, `agent`, `skill`, `pty`. +`createOpencodeClient` takes `directory` and injects it as both the `x-opencode-*` headers and the +`location[directory]` query param — the thing we hand-roll in two places. + +That would delete: our hand-rolled SSE reader, both envelope unwrappers, three hand-written type sets, +and the model-id string splitting. It is a dependency change, and installs here are frozen, so it is a +deliberate `bun install --no-frozen-lockfile` plus a read of the lockfile diff. Worth noting the +package's only dependency is `cross-spawn`. + +Not to be confused with two siblings the v2 docs mention: `@opencode-ai/sdk-next` is marked private and +is not on npm, and `@opencode-ai/client` is a private generation target for the beta line. + +--- + ## 8. What we'd gain immediately Ordered by value over effort. 1–3 are bug fixes, not features. @@ -326,16 +411,27 @@ Then the two that are real features needing UI: **permissions/questions** (§4d) 1. Is there a field that says which engine owns a session? Tonight's only discriminator is behavioural (legacy returns `[]`). If not, we need our own record — we already store `sessionKey → ses_…` in `opencode/state.ts` and could record the surface with it. -2. Do the `v2` permission/question events replace the v1 ones or run alongside? Both are emitted; - which one a 1.18.16 agent actually uses is unconfirmed. +2. Which permission/question family does a 1.18.16 agent actually emit? Both are declared and both + appear in our `/doc`, because the server wires the full manifest. Measure before building UI. 3. What is `/api/*`'s auth story? The spec declares no `securitySchemes` yet every route declares a `401`. The v1 docs describe HTTP Basic via `OPENCODE_SERVER_PASSWORD`; we run with none, on loopback. In the 2.0 beta this is formalised as basic auth read from `~/.local/state/opencode/service.json`. -4. Does `@opencode-ai/sdk/v2` work against 1.18.16 exactly? Generated from the same surface, but - unverified by running it. -5. When did `/api/*` first appear in the 1.x line, and when does the legacy surface actually go? Both - UNKNOWN — the published changelog names no `/api/` additions at all. +4. Does `@opencode-ai/sdk/v2` work against 1.18.16 exactly? It is generated from this exact server's + OpenAPI output and versioned in lockstep, so it should — but nobody here has run it. +5. When did `/api/*` first appear in the 1.x line? UNKNOWN; the changelog names no `/api/` additions. + The event family underneath it landed in 1.15.0. And no dated removal plan exists for `/session/*`, + `permission.asked/replied` or `question.*` — only the undated internal intent quoted in §2. +6. Will the `v2`-branch event renames reach the 1.x line, or only ship with OpenCode 2.0? No merge + found, no statement either way. This decides whether the mapping table in §2 is a one-off or a + permanent seam. +7. Is a durable `seq` stable across a server restart or a session move? It is a database column, so it + should be, but no durability guarantee is documented and we have not tested it. Item 6 in §8 depends + on the answer. + +Version state at the time of writing: **1.18.16 is the newest release** (2026-08-10) and contains +nothing API-facing. The active stream is the 2.0 beta, cutting releases continuously — the most recent +was published hours before this file was written. --- @@ -345,9 +441,17 @@ Then the two that are real features needing UI: **permissions/questions** (§4d) above against the local sidecar's serve on port 49698. - Code: `src/servers/sidecar/opencode/*`, `src/servers/api/chat/opencode/*`, `opencode-sessions.ts`, `list-models.ts`, `send-opencode.ts`. -- Upstream: opencode.ai/v2/docs/migrate-v1, opencode.ai/v2/docs, opencode.ai/docs/server, - github.com/anomalyco/opencode (formerly sst/opencode), npm `@opencode-ai/sdk` 1.18.16, - `@opencode-ai/client@next`. +- Upstream, docs: opencode.ai/v2/docs/migrate-v1, opencode.ai/v2/docs, opencode.ai/v2/docs/permissions, + opencode.ai/docs/server, opencode.ai/changelog. +- Upstream, source at tag `v1.18.16` in **github.com/anomalyco/opencode** (formerly `sst/opencode`, + which 301s): `packages/protocol/src/api.ts` and `groups/session.ts` (the surface's own "experimental" + self-description, the `after` parameter), `packages/schema/src/session-event.ts` (`DurableDefinitions` + vs `Definitions` — the delta exclusion), `packages/schema/src/{permission,question}.ts` and their + `v1/` counterparts, `packages/schema/src/session-input.ts` (`admittedSeq`), `packages/schema/AGENTS.md` + (the V1/V2 naming intent), `packages/core/src/event.ts` (replay-then-live), `packages/sdk/js/script/ + build.ts` and `src/v2/client.ts`. PRs #27415 (the engine landing in 1.15.0), #33993, #35217, #35229 + (the renames). +- npm: `@opencode-ai/sdk` 1.18.16, `@opencode-ai/client@next`. - Prior art in this repo: `docs/opencode-parity.md`, `-fork-decision.md`, `-serve-migration-plan.md`, `-serve-path.md`, `-testing-checklist.md`, `-phase0-review.md`, `-phase1-report.md`, `-phase1-review.md`.