Skip to content

refactor(client): group experimental SDK methods under client.experimental - #59072

Open
kitlangton wants to merge 19 commits into
devfrom
kit/sdk-experimental-namespace
Open

kitlangton wants to merge 19 commits into
devfrom
kit/sdk-experimental-namespace

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

In the SDK, experimental methods now live under client.experimental, so you can tell them apart from stable ones.
Today client.session.stats() (experimental) sits right next to client.session.get() (stable). URLs and wire formats don't change, so shipped apps and older CLIs keep working. Ships as a minor bump with deprecations, not removals: every old SDK method and type name still works as a @deprecated alias of its new home until the next minor.

❌ Before

  1. Every route under /api/experimental/ was generated into its normal group: client.session.stats(), client.voice.models(), client.mcp.connect().
  2. Nothing in the method name or type says it might change ❌.

✅ After

  1. The generator nests any route under /api/experimental/ into client.experimental: client.experimental.session.stats(), client.experimental.voice.models(), client.experimental.mcp.connect().
  2. Stable groups contain only stable methods; new routes are sorted automatically.
  3. Generated type names gain an Experimental prefix (ExperimentalSessionStatsOutput).
  4. Experimental OpenAPI identifiers all start with experimental., matching the path (12 didn't: session.log and 11 persistentPty.*).
- client.session.stats()           // experimental, looks stable
- client.voice.models()
+ client.experimental.session.stats()
+ client.experimental.voice.models()
  codegen: namespace(endpoint) = route starts with /api/experimental/ ? "experimental" : none

🕰️ Deprecated aliases, generated (removed in the next minor): a new codegen option, compile(api, { aliases: true }), keeps every old path and type name as a @deprecated alias of its new home. The alias is the same function object, and its JSDoc names the replacement and says "Removed in the next minor". packages/client/script/generate.ts turns it on; nothing is hand-written.

  • Methods (Promise and Effect clients): client.session.{stats,import,export,skill,wait,log}, client.session.instructions.entry.*, client.generate.text, client.voice.{models,session}, client.migration.v1.status, client.usage.limits, client.integration.wellknown.add, client.mcp.{add,remove,connect,disconnect}, client.mcp.client.token, client.file.{watch,unwatch}, client.config.update, and client.file.write (Promise client only).
  • Types: 46 Promise and 71 Effect types keep their old names (SessionWaitInput = ExperimentalSessionWaitInput). The Effect group interfaces (SessionApi, IntegrationApi, McpApi, FileApi, ConfigApi, AppApi) keep the old members, so code typed against them still compiles, and whole moved groups keep their names (GenerateApi, VoiceApi, MigrationApi, UsageApi).
  • Removing them later: drop aliases: true and regenerate.

🧩 Plugin SDK: context names are unchanged (ctx.generate, ctx.session.wait, ctx.session.instructions stay where they are). Its types now point at the Experimental* SDK types, so plugins don't touch anything deprecated.

🧹 Remove at next minor: the generated aliases and their tests.

  • Methods: client.session.{stats,import,export,skill,wait,log}, client.session.instructions.entry.{list,put,remove}, client.generate.{text,image,decisions}, client.voice.{models,session}, client.migration.v1.status, client.usage.limits, client.integration.wellknown.add, client.mcp.{add,remove,connect,disconnect}, client.mcp.client.token, client.file.{watch,unwatch}, client.config.update, and client.file.write (Promise only). Each old path is now client.experimental.<same path>.
  • Types: the Input, Output and (Effect) Operation types of those methods (old SessionWaitInput, new ExperimentalSessionWaitInput), plus the Effect GenerateApi, VoiceApi, MigrationApi, UsageApi and the Promise GenerateApi in packages/client/src/promise/api.ts.
  • Steps: (1) drop aliases: true from both compile calls in packages/client/script/generate.ts and run bun run generate in packages/client; (2) delete GenerateApi from packages/client/src/promise/api.ts; (3) delete packages/client/test/deprecated-aliases.test.ts and the alias entries in packages/client/test/promise.test.ts; (4) drop the Omit<IntegrationApi, "wellknown"> in packages/plugin/src/{effect,promise}/integration.ts.

🚢 Release: ship as a minor. The repo has no changesets; dispatch release.yml on dev with bump: minor.

🟰 Behavior change: none on the wire, and no breaking change for SDK or plugin callers: old paths keep working and show as deprecated in editors. Only OpenAPI operation ids change for the 12 renamed endpoints; their generated SDK methods don't. All in-repo callers use the new paths (app, TUI, desktop, CLI, GUI extensions, server and SDK tests), including ones added on dev since the PR opened (voice, file watch, media, instruction entries).

📊 Proof: the tests are derived from real data, not a hand-kept list:

Test Checks Mismatches
client: calls every generated method under client.experimental ⇔ experimental route; every other path to one is exactly the 24 known deprecated aliases 0
client deprecated-aliases every alias the codegen reports is the same function (toBe) as its client.experimental path, Promise and Effect 0
codegen: aliases old paths and types emitted with @deprecated docs, call the experimental URL, off by default; collisions fail generation 0
protocol experimental-identifiers path under /api/experimental ⇔ id starts with experimental. 12 found, fixed

👀 Read: packages/protocol/src/client.ts (clientNamespace) and, in packages/httpapi-codegen/src/index.ts, the tree rendering plus aliases (renderAliasedClient, renderPromiseTypes, renderEffectShape). The rule goes by URL, not operation id, because the URL is what callers depend on. Name collisions anywhere in the tree, aliases included, fail generation. Some of these endpoints already back our own UI and may be worth promoting to stable later (session.stats/wait/export/import/log, usage.limits, voice.*, mcp.connect/disconnect/add, config.update, migration.v1.status, integration.wellknown.add); none are promoted here. The rest is generated code and caller renames.

No user reports · brought up to date by merging dev (the branch already used merges); generated files rebuilt with the repo's generators

Checks run
  • bun run generate in packages/protocol and packages/client; bun run check:generated in both: clean
  • bun test (packages/client): 241 pass
  • bun test (packages/httpapi-codegen): 103 pass
  • bun test test/plugin (packages/core): 439 pass (6 DigitalOcean provider tests fail locally on a missing localhost auth server; file unchanged from dev, green in CI)
  • bun turbo typecheck: 35/35 pass

Added by the opencode bot (AI-generated).

Public API: adds client.experimental.* with generated @deprecated aliases at the old SDK paths and type names (backwards compatible until the next minor); plugin context unchanged; renames 12 experimental OpenAPI operationIds to experimental.*

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks sound. check:generated is clean, the client and httpapi-codegen tests pass (235 and 100), and so do the caller tests in cli, tui, sdk and server. A search found no callers still using the old method paths. Running the CLI from source failed on the base and the PR alike (missing react/jsx-dev-runtime in this environment), so I did no end-to-end CLI or TUI run.

Public API impact (promise and Effect clients; not backwards compatible for experimental methods): every method whose route is under /api/experimental/ moves from client.<group>.<method> to client.experimental.<group>.<method>:

  • session.{stats, import, export, skill, wait, instructions, log} → experimental.session.*
  • generate.text → experimental.generate.text
  • voice.{models, session} → experimental.voice.*
  • integration.wellknown.add → experimental.integration.wellknown.add
  • mcp.{add, remove, connect, disconnect, client.token} → experimental.mcp.*
  • file.write → experimental.file.write
  • migration.v1.status → experimental.migration.v1.status
  • usage.limits → experimental.usage.limits
  • config.update → experimental.config.update

The top-level generate, voice, migration and usage groups are removed. Exported types are renamed with an Experimental prefix, e.g. GenerateApi → ExperimentalGenerateApi, and ExperimentalSessionApi is new. There are no aliases, so external SDK callers must add .experimental. HTTP routes, wire formats and the plugin context (ctx.generate, ctx.session.wait) are unchanged.

The description's list of moved methods leaves out session.skill, session.instructions, mcp.remove, mcp.client.token and file.write. Worth adding them to the release notes so SDK users aren't surprised.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commits look fine.

  • Codegen fix: projected Effect clients now read each group from the raw client by its client name, so nested groups such as experimental.session resolve. The in-repo build doesn't use projections, so the generated output is unchanged (check:generated is clean). The new test covers the case.
  • Plugin types: IntegrationDomain no longer needs to omit wellknown, since that group has moved to client.experimental.integration.
  • Docs: the client and plugin pages now use the new names.

Checks: codegen tests (100) and client tests (235) pass, and plugin type-checks cleanly with tsc.

The public API impact is the same as in the earlier review: experimental client methods and their types move under client.experimental / the Experimental prefix. The plugin IntegrationDomain type change doesn't change what plugins can call.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only clears lint warnings in packages/app, and behaviour is unchanged. The MCP toggle gets the same server name, location, pending check and enabled state as before for both forms of input. packages/app type-checks cleanly with tsc. Nothing new to raise.

The public API impact is unchanged from the earlier review: experimental client methods and their types move under client.experimental / the Experimental prefix.

…amespace

# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
#
# Conflicts:
#	packages/app/src/settings/general/controllers.ts
#	packages/client/src/promise/generated/client.ts
#	packages/client/src/promise/generated/types.ts
#	packages/client/test/promise.test.ts
#	packages/plugin/src/effect/session.ts
#	packages/plugin/src/promise/session.ts
#	services/www/src/docs/content/build/plugins/effect.mdx
@kitlangton kitlangton changed the title refactor(client): nest experimental endpoints under client.experimental refactor(client): group experimental SDK methods under client.experimental Oct 26, 2026

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commits look fine. Callers that arrived from dev now use the experimental paths: app voice (experimental.voice.*, experimental.session.wait), file watching (experimental.file.watch/unwatch) and the server media test (experimental.session.import/export). The plugin SessionDomain now takes instructions as well as wait from the experimental session API, so ctx.session.instructions keeps its name. A search found no callers left on the old paths.

Checks: client tests (239) and codegen tests (101) pass, check:generated is clean, media.test.ts passes, and app, plugin and gui-extensions type-check cleanly with tsc.

The public API impact is the same as in the earlier review, now also covering file.watch/file.unwatch → experimental.file.watch/unwatch. All experimental methods move under client.experimental, with no aliases.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only clears lint warnings in packages/app and doesn't change behaviour. VoiceTransport.send() now takes a typed VoiceClientEvent union instead of object, and the rest is blank lines. packages/app type-checks cleanly with tsc, so all existing send() calls fit the union, and the voice tests (29) pass. Nothing new to raise.

The public API impact is unchanged from the earlier review: experimental client methods and their types move under client.experimental / the Experimental prefix.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit looks fine. Endpoint names are unchanged, so server handlers, the codegen skip lists and the generated SDK method names are unaffected: check:generated in packages/client is clean. openapi.json is in sync with its generator. The new protocol test, and the server, CLI api and codemode tests, pass.

New public API change: OpenAPI operation ids persistentPty.{read, list, create, shutdown, handoff, get, update, snapshot, remove, connectToken, connect} → experimental.persistentPty.*, and session.log → experimental.session.log. URLs, payloads and client method names are unchanged. Anything that looks up operations by id must switch to the new names: opencode api <operationId> (e.g. opencode api session.log → opencode api experimental.session.log), the docs' API reference anchors, and SDKs generated from openapi.json in other languages. These endpoints are all experimental, so this isn't blocking, but it may be worth a line in the description's behaviour-change note.

The SDK-level impact is the same as in the earlier reviews: experimental client methods and their types move under client.experimental / the Experimental prefix, with no aliases.

…amespace

# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
#
# Conflicts:
#	packages/client/script/build.ts
#	packages/client/src/promise/generated/types.ts
…imental

ctx.generate, ctx.session.wait and ctx.session.instructions move to ctx.experimental.{generate,session.wait,session.instructions} in both the Effect and Promise plugin Context, mirroring client.experimental. No aliases. A core test derives the rule from the Promise adapter's endpoint mapping.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugin commit looks right. The three groups of plugin methods move under ctx.experimental in both plugin contexts (Effect and Promise), core's plugin host and the Promise adapter. The instruction-entry methods keep their session and key checks unchanged. A search found no plugin code left on the old paths.

Checks: the new experimental-namespace and session-instructions tests in packages/core pass, as do the packages/plugin tests. check:generated is clean in packages/client and packages/protocol. packages/plugin, packages/core and packages/app type-check with no new errors.

New public API change (plugin API, breaking, no aliases): ctx.generate.text → ctx.experimental.generate.text, ctx.session.wait → ctx.experimental.session.wait, ctx.session.instructions.entry.{list,put,remove} → ctx.experimental.session.instructions.entry.*. This applies to both plugin contexts (Effect and Promise). Existing plugins that call these must add .experimental. The SDK and OpenAPI operation-id changes are as in the earlier reviews.

One small point inline: adaptedEndpoints is test-only state that the plugin package now exports publicly.

Comment thread packages/plugin/src/promise/adapter.ts Outdated
…liases

Old client and plugin paths forward to client.experimental / ctx.experimental until the next minor. Adds .github/RELEASE_REMINDERS.md and the AGENTS.md rule for deprecating shipped SDK and plugin API.
@kitlangton
kitlangton requested a review from a team as a code owner October 26, 2026 23:14

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecated aliases work as described. The old SDK method paths point to the same functions as their client.experimental.* homes, in both the Promise and Effect clients. The old Promise and Effect type names are kept as @deprecated aliases. Core's plugin host and the Promise adapter expose the same experimental methods at ctx.experimental.* and at the deprecated ctx.generate, ctx.session.wait and ctx.session.instructions, with unchanged bodies.

Checks: client tests (240) pass and check:generated is clean. The core plugin experimental-namespace and session-instructions tests pass, as do the plugin tests. Client, plugin, core and app type-check with no new errors.

The earlier point about adaptedEndpoints being exported from @opencode/plugin/promise/adapter is not addressed yet.

Public API: the SDK and plugin moves are now backwards compatible through @deprecated aliases until the next minor. Additions: client.experimental.*, ctx.experimental.{generate, session.wait, session.instructions}, and the Experimental* type names. Unchanged from before: the OpenAPI operation-id renames (persistentPty.* and session.log → experimental.*) still have no alias for id-based callers such as opencode api <operationId>.

One gap: in the Effect client, the old paths exist on the client object (OpenCodeClient) but not on the generated group interfaces. AppApi has no generate, voice, usage or migration; SessionApi has no stats or wait; and McpApi, ConfigApi, IntegrationApi and FileApi are missing their moved methods. Effect code that names those types (e.g. SessionApi<E>["wait"] or AppApi<E>["generate"]) won't compile even though the call works at runtime. The Promise group types are built from the client object, so they're unaffected. If the goal is no type-level breaks either, the deprecated members could be added to those Effect interfaces too.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit closes the gap from my last review. The Effect AppApi, SessionApi, IntegrationApi, McpApi, FileApi and ConfigApi interfaces now carry the old members as @deprecated entries, typed from their Experimental* replacements. Effect code typed against them keeps compiling. Nothing in the repo implements these interfaces in a way the extra members would break.

Checks: client, plugin, core and app type-check with no new errors; sdk, server and cli show the same errors as the base; client tests (240) pass.

The SDK and plugin moves are now backwards compatible through deprecated aliases until the next minor, at both runtime and type level. The OpenAPI operation-id renames are unchanged from before. The earlier point about adaptedEndpoints being exported is still open.

compile(..., { aliases: true }) keeps each pre-experimental client path and type name as a @deprecated alias of its client.experimental replacement, removed in the next minor. Replaces the hand-written alias files and reverts the plugin context to its dev names.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit fixes my open thread: adaptedEndpoints and the test that needed it are gone, so the plugin package no longer exports test-only state.

The deprecated aliases are now generated by the codegen (aliases: true) instead of hand-written. Checks: the new deprecated-aliases test confirms every old path on the real Promise and Effect clients is the same function as its client.experimental path. Client tests (241) and codegen tests (103) pass, check:generated is clean, and client, plugin, core and app type-check with no new errors.

Public API: the old SDK method paths, type names and Effect group members stay as @deprecated aliases until the next minor, so the SDK change is backwards compatible. The plugin context API is unchanged (ctx.generate, ctx.session.wait and ctx.session.instructions stay put; only their types point at Experimental*). The only breaking change left is the renamed OpenAPI operation ids for 12 endpoints (persistentPty.* and session.log → experimental.*), which affects anything that looks operations up by id, such as opencode api <operationId>.

…amespace

# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
#
# Conflicts:
#	packages/client/src/promise/generated/types.ts

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update only merges dev; the pull request's own changes are as I last reviewed them. The regenerated openapi.json and Promise types drop IntegrationInfo.configured, from the revert of "fix(tui): identify config-defined providers" (#59439) on dev, not from this pull request. check:generated is clean in packages/client and packages/protocol, and the client tests (241) pass.

The earlier point about the exported adaptedEndpoints is fixed, since it is no longer in the plugin package. Nothing new to raise; the public API impact is the same as in my previous review.

…amespace

# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
#
# Conflicts:
#	services/www/src/docs/content/build/plugins/index.mdx

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update only merges dev, and the pull request's own changes still look right after it. The renamed gui-plugins server panel and the moved build/plugins/server.mdx docs keep the experimental paths and types. dev's new stable GET /api/plugin/gui was generated as client.plugin.gui, outside client.experimental.

Checks: check:generated is clean in packages/client and packages/protocol. The protocol tests (10) and client tests (241, including the placement test) pass. app, gui-plugins and plugin type-check cleanly with tsc.

Nothing new to raise; the public API impact is the same as in my previous review.

…amespace

# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
#
# Conflicts:
#	packages/client/src/effect/api/api.ts
#	packages/client/src/effect/generated/client.ts
#	packages/client/src/promise/generated/client.ts
#	packages/client/src/promise/generated/types.ts
#	packages/client/test/promise.test.ts
#	packages/plugin/src/effect/plugin.ts
#	packages/plugin/src/promise/plugin.ts
#	packages/protocol/src/client.ts
#	services/www/src/docs/content/build/plugins/effect.mdx

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant