Skip to content

Commit 9c20973

Browse files
committed
refactor(client): generate from hosted session group
1 parent 07a65d5 commit 9c20973

12 files changed

Lines changed: 411 additions & 86 deletions

File tree

CONTEXT.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ _Avoid_: Response envelope
137137
- Creating **Embedded OpenCode** is scoped. Closing its owning Scope releases the in-process server resources, database resources, registrations, and fibers.
138138
- **Embedded OpenCode** exposes shared client capabilities and embedded-only capabilities on one object; consumers do not navigate through a nested `.client` property.
139139
- The beta **OpenCode Client** currently uses plural consumer-facing capability groups such as `sessions`; whether the stable Session namespace should instead be singular `session` must be settled before stabilization. Internal server identifiers do not implicitly define public client names.
140-
- The public `HttpApi` is authoritative for shared **OpenCode Client** capabilities: the server and code generation reuse the same endpoint declaration objects. As a temporary beta compromise, generation composes a lightweight projection group from selected endpoints because importing the hosted Session group currently reaches heavy Core and server runtime modules.
140+
- The public `HttpApi` is authoritative for shared **OpenCode Client** capabilities: the server and code generation consume the same hosted `SessionGroup`. Codegen may assign a separate consumer-facing group name without reconstructing group membership or endpoint contracts.
141141
- SDK generation reflects the public `HttpApi` once into an **SDK Contract IR**. Promise and Effect emitters share endpoint structure and transport metadata without being required to expose identical public values: an emitter may select encoded wire types, decoded domain types, compile-time brands, runtime validation, and its own execution abstraction independently.
142142
- The first Effect emitter is the rich projection: it exposes decoded Effect-native values, preserves brands and schema transformations, performs runtime schema decoding, and delegates transport interpretation to `HttpApiClient`. Lighter wire-shaped Effect output remains possible through another emitter policy rather than constraining the shared IR.
143143
- The rich Effect emitter regenerates private executable schemas when the **SDK Contract IR** proves that their transport semantics can be reproduced exactly. Contracts with authoritative custom transformations use the import-based Effect emitter against the canonical V2 server `HttpApi`; the Promise emitter still derives zero-Effect structural wire types from the same IR.
144-
- `@opencode-ai/server` owns the authoritative V2 `HttpApi`. The hosted server group remains authoritative; the temporary generated projection must reuse its endpoint declarations exactly and must not independently redefine their HTTP contracts.
144+
- `@opencode-ai/server` owns the authoritative V2 `HttpApi`. The server and client generator consume the exact hosted `SessionGroup`, including `compact`, `wait`, and `context`.
145145
- The first Promise emitter targets the same clean domain-oriented method organization rather than Hey API source compatibility. It returns unwrapped values directly, rejects declared and infrastructure failures, and begins with minimal client-level transport configuration; result wrappers, interceptors, and legacy generated signatures are outside the initial surface.
146146
- The first Promise emitter parses response syntax and trusts its generated structural types; it does not perform runtime structural validation. Malformed payload syntax fails, while a syntactically valid shape mismatch is not detected at the SDK boundary. Standalone validator generation remains an optional future emitter policy.
147147
- Declared Promise-client failures retain their tagged structural wire values and have generated type guards. Consumers do not depend on generated `Error` subclass identity, preserving discrimination across package copies and realms while remaining structurally aligned with Effect domain errors.
@@ -153,7 +153,7 @@ _Avoid_: Response envelope
153153
- Promise client construction is synchronous and network-free. It requires `baseUrl`, defaults to `globalThis.fetch`, accepts client-level headers, and merges them with per-call header overrides.
154154
- Effect client construction accepts an explicit `baseUrl` and obtains `HttpClient.HttpClient` from the Effect environment. It does not install fetch or duplicate per-call transport policy; callers transform/provide the client for headers, tracing, retries, recording, and tests, while fiber interruption owns cancellation.
155155
- Promise and Effect emitters each own their generated public type modules. The **SDK Contract IR**, not a physically shared generated type package, is the common source; this permits zero-Effect wire types and rich decoded Effect types to evolve independently.
156-
- Promise and Effect client outputs ship from `@opencode-ai/client` behind isolated root, `/effect`, and `/effect/embedded` exports. The root export has no runtime path to Effect; `/effect` contains only the rich network client and may use Effect as an optional peer dependency; `/effect/embedded` owns the heavy private Core/server dependency closure for the scoped embedded host. The package remains private until the authoritative public `HttpApi` is composed and packaging of embedded dependencies is settled.
156+
- Promise and Effect client outputs ship from `@opencode-ai/client` behind isolated root, `/effect`, and `/effect/embedded` exports. The root export has no runtime path to Effect. During alpha, `/effect` imports the authoritative hosted group and therefore accepts its heavy private Core/server dependency closure; `/effect/embedded` additionally owns the scoped in-process host. The package remains private until the contract import graph and embedded packaging are settled.
157157
- A capability intended for both networked and **Embedded OpenCode** belongs in the authoritative public `HttpApi`; embedded-only same-process capabilities extend **Embedded OpenCode** separately.
158158
- `sessions.events({ sessionID, after })` is a public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes.
159159
- `events.subscribe()` is a distinct public instance-wide live stream for Session and non-Session activity. It has no replay guarantee and includes connection, heartbeat, and instance-disposal lifecycle events; consumers recover from disconnection by refreshing authoritative state.
@@ -192,19 +192,17 @@ _Avoid_: Response envelope
192192

193193
## Deferred client contract cleanup
194194

195-
The beta client currently reconstructs a small `HttpApi`/group projection from authoritative Session endpoint objects. This avoids pulling the full Core/server runtime graph into `@opencode-ai/client/effect`, but group membership, group annotations, and the client namespace are consequently maintained outside the hosted `SessionGroup`. This is an accepted beta compromise, not the intended stable boundary.
195+
The alpha client imports the exact hosted `SessionGroup`. This keeps server hosting and client generation structurally aligned, but currently pulls the full Core/server runtime graph into `@opencode-ai/client/effect`. That cost is accepted during alpha; the intended stable boundary keeps the same authoritative group while isolating its contract imports.
196196

197197
Before stabilizing the client API:
198198

199199
- Isolate runtime HTTP contract values into lightweight leaf modules. Importing Session, prompt, admission, message, model, location, and related schemas must not transitively load databases, Drizzle, Session execution, providers, watchers, native modules, or WASM.
200200
- Separate the `SessionLocationMiddleware` service tag and error contract from its database-backed layer implementation.
201-
- Export one lightweight authoritative `SessionGroup` and have both the server API and client codegen import that exact group value.
202-
- Remove the endpoint-map projection and generated shadow `HttpApiGroup` once the authoritative group is browser-safe to import.
203-
- Generate all appropriate shared Session operations from that group, including `compact`, `wait`, and `context`, rather than maintaining a selected six-endpoint list.
201+
- Make the existing authoritative `SessionGroup` browser-safe to import without changing server or generated client behavior.
204202
- Project the existing list response envelope to the stable client **Page** shape and enforce separate initial-query and cursor-continuation inputs without changing the hosted V2 wire contract.
205203
- Settle the stable consumer namespace (`session` versus the current beta `sessions`) and use an explicit codegen annotation if the consumer name should differ from the server group identifier.
206204
- Preserve V2 route paths, operation IDs, codecs, errors, middleware behavior, and OpenAPI output while making this change.
207-
- Verify the isolated `@opencode-ai/client/effect` browser bundle does not include heavy embedded dependencies; those remain owned by `@opencode-ai/client/effect/embedded`.
205+
- Restore a browser-safe `@opencode-ai/client/effect` bundle after contract import isolation; until then only the zero-Effect Promise root has a lightweight portability guarantee.
208206
- Define embedded-host placement before supporting multiple hosts over one database. Hosts that share durable Session storage must also share process-local Session execution coordination, or each host must receive isolated storage explicitly.
209207
- Keep an embedded request scope alive until any streamed response body finishes. The initial non-streaming Session surface does not exercise this lifetime boundary; Session and instance event streams must do so before joining the embedded client.
210208

packages/client/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Private generation target for clients derived directly from OpenCode's authorita
88
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.
99
- `@opencode-ai/client/effect/embedded`: scoped embedded OpenCode host backed by Core and the in-memory HTTP router.
1010

11-
The initial generated surface contains `sessions.list`, `create`, `get`, `switchAgent`, `switchModel`, and `prompt`. The server and generator reuse the same endpoint declarations, while generation temporarily composes a lightweight projection group to keep the network Effect entrypoint isolated from the heavy Core/server runtime graph. Run `bun run generate` after changing that contract and `bun run check:generated` to detect committed-output drift.
11+
The generated surface contains `sessions.list`, `create`, `get`, `switchAgent`, `switchModel`, `prompt`, `compact`, `wait`, and `context`. The server and generator consume the exact same hosted `SessionGroup`. Run `bun run generate` after changing that contract and `bun run check:generated` to detect committed-output drift.
1212

13-
This projection is a beta implementation compromise. The intended stable design is one browser-safe authoritative `SessionGroup`, imported directly by both the server and codegen after its schema and middleware-tag dependencies have been isolated into lightweight modules. The migration checklist is recorded in `CONTEXT.md` under "Deferred client contract cleanup."
13+
During alpha, the Effect network entrypoint accepts the authoritative group's heavy Core/server import graph. The intended stable design keeps the same group while isolating its schema and middleware-tag dependencies into lightweight modules. The migration checklist is recorded in `CONTEXT.md` under "Deferred client contract cleanup."
1414

1515
The embedded entrypoint exposes a scoped host backed by the same server router, middleware, handlers, and HTTP codecs as the network client:
1616

packages/client/script/build.ts

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,20 @@
11
import { NodeFileSystem } from "@effect/platform-node"
2-
import {
3-
SessionsCreate,
4-
SessionsGet,
5-
SessionsList,
6-
SessionsPrompt,
7-
SessionsSwitchAgent,
8-
SessionsSwitchModel,
9-
} from "@opencode-ai/server/groups/session-endpoints"
2+
import { SessionGroup } from "@opencode-ai/server/groups/session"
103
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
114
import { Effect } from "effect"
12-
import { HttpApi, HttpApiGroup } from "effect/unstable/httpapi"
5+
import { HttpApi } from "effect/unstable/httpapi"
136

14-
const Api = HttpApi.make("opencode-client").add(
15-
HttpApiGroup.make("sessions")
16-
.add(SessionsList)
17-
.add(SessionsCreate)
18-
.add(SessionsGet)
19-
.add(SessionsSwitchAgent)
20-
.add(SessionsSwitchModel)
21-
.add(SessionsPrompt),
22-
)
23-
const contract = compile(Api)
7+
const Api = HttpApi.make("opencode-client").add(SessionGroup)
8+
const contract = compile(Api, { groupNames: { "server.session": "sessions" } })
249

2510
await Effect.runPromise(
2611
Effect.all(
2712
[
2813
write(emitPromise(contract), new URL("../src/generated", import.meta.url).pathname),
2914
write(
3015
emitEffectImported(contract, {
31-
module: "@opencode-ai/server/groups/session-endpoints",
32-
endpoints: {
33-
"sessions.session.list": "SessionsList",
34-
"sessions.session.create": "SessionsCreate",
35-
"sessions.session.get": "SessionsGet",
36-
"sessions.session.switchAgent": "SessionsSwitchAgent",
37-
"sessions.session.switchModel": "SessionsSwitchModel",
38-
"sessions.session.prompt": "SessionsPrompt",
39-
},
16+
module: "@opencode-ai/server/groups/session",
17+
group: "SessionGroup",
4018
}),
4119
new URL("../src/generated-effect", import.meta.url).pathname,
4220
),

0 commit comments

Comments
 (0)