cursor-opencode-provider 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +50 -180
  2. package/dist/compaction-marker.d.ts +3 -2
  3. package/dist/compaction-marker.js +3 -2
  4. package/dist/host-agent-mode.d.ts +32 -0
  5. package/dist/host-agent-mode.js +92 -0
  6. package/dist/language-model.d.ts +6 -1
  7. package/dist/language-model.js +170 -34
  8. package/dist/models.d.ts +12 -1
  9. package/dist/models.js +16 -6
  10. package/dist/opencode2/catalog.d.ts +46 -11
  11. package/dist/opencode2/catalog.js +60 -45
  12. package/dist/opencode2/todo-tools.d.ts +64 -0
  13. package/dist/opencode2/todo-tools.js +165 -0
  14. package/dist/opencode2/types.d.ts +138 -83
  15. package/dist/opencode2/types.js +8 -10
  16. package/dist/plan-execution-kickoff.d.ts +2 -0
  17. package/dist/plan-execution-kickoff.js +25 -1
  18. package/dist/plugin-opencode2.d.ts +4 -1
  19. package/dist/plugin-opencode2.js +335 -154
  20. package/dist/plugin.js +5 -1
  21. package/dist/protocol/conversation-persistence.d.ts +2 -0
  22. package/dist/protocol/conversation-persistence.js +19 -1
  23. package/dist/protocol/conversation-state.d.ts +4 -0
  24. package/dist/protocol/conversation-state.js +4 -0
  25. package/dist/protocol/switch-mode.d.ts +5 -4
  26. package/dist/protocol/switch-mode.js +12 -4
  27. package/dist/protocol/tool-call-bridge.js +1 -1
  28. package/dist/protocol/tools.d.ts +30 -4
  29. package/dist/protocol/tools.js +185 -58
  30. package/dist/session.d.ts +7 -1
  31. package/dist/shared.d.ts +3 -1
  32. package/dist/shared.js +3 -1
  33. package/dist/shell-timeout.d.ts +13 -0
  34. package/dist/shell-timeout.js +38 -0
  35. package/dist/todo-store.d.ts +29 -0
  36. package/dist/todo-store.js +78 -0
  37. package/dist/web-tools.d.ts +13 -0
  38. package/dist/web-tools.js +83 -5
  39. package/package.json +6 -3
package/README.md CHANGED
@@ -18,8 +18,8 @@ instead of requiring a host-specific Cursor provider.
18
18
 
19
19
  | Coding agent | How this package runs |
20
20
  |---|---|
21
- | **OpenCode** | Native plugin and AI SDK provider |
22
- | **OpenCode 2.0** | Native dedicated `cursor-opencode-provider/plugin/opencode2` entrypoint |
21
+ | **OpenCode** | Native plugin and AI SDK provider — [OpenCode 1.x setup](docs/opencode-1.md) |
22
+ | **OpenCode 2.0** | Native `cursor-opencode-provider/plugin/opencode2` — [OpenCode 2.0 setup](docs/opencode-2.md) |
23
23
  | **Kilo Code** | Unchanged through OCP's OpenCode-clone compatibility layer |
24
24
  | **MiMo Code** | Unchanged through OCP's OpenCode-clone compatibility layer |
25
25
  | **pi** | Unchanged through `@opencode-compat/pi-bridge` and pi's provider extension API |
@@ -48,7 +48,7 @@ OpenCode driving a Cursor-routed Grok model through this provider:
48
48
  - **Model discovery** — fetches available models from Cursor's API and caches them locally
49
49
  - **Image input** — advertises vision only for supported models and forwards OpenCode image attachments to Cursor
50
50
  - **Streaming** — bidirectional Connect-RPC Runs with stale-session rotation, health checks, semantic/read-idle deadlines, bounded replay-safe recovery, and activity-aware held tool continuations
51
- - **Tool calls** — maps Cursor exec-server messages to AI SDK / OpenCode tool-call parts, including catalog-aware native subagent execution: exact advertised custom agents win, `unspecified` / `generalPurpose` select `general`, read-oriented `bugbot` / `security-review` select `explore`, and `cursor-guide` selects enabled Kilo `scout` before `explore`; MiMo `actor` is used instead of its work-item `task` tool when advertised. The bridge also covers the Pi read/bash/edit/write/grep/find/ls request/result range, enforces the exact current OpenCode tool catalog, mirrors finalized display-only todo/plan state, and strips OpenCode's `read` XML envelope before returning content to Cursor.
51
+ - **Tool calls** — maps Cursor exec-server messages to AI SDK / OpenCode tool-call parts using only the advertised canonical catalog. Exact advertised subagent names win; standard Cursor roles select compatible OpenCode agents, and native Task maps to OpenCode 1.x `task` or OpenCode 2.0 `subagent`. OCP translates any alternate host vocabulary before it reaches this package. The bridge also covers Cursor's typed read/bash/edit/write/grep/find/ls request/result range, mirrors finalized display-only todo/plan state, and strips OpenCode's `read` XML envelope before returning content to Cursor.
52
52
  - **Thinking / reasoning** — surfaces extended-thinking deltas where the model supports it
53
53
 
54
54
  ## Requirements
@@ -59,34 +59,34 @@ OpenCode driving a Cursor-routed Grok model through this provider:
59
59
 
60
60
  ## Installation
61
61
 
62
- ### From npm
62
+ This package is one plugin with host-specific entrypoints. Use the guide for your OpenCode major version:
63
63
 
64
- Add the package to OpenCode config. OpenCode installs npm plugins with Bun at startup (cached under `~/.cache/opencode/node_modules/`):
64
+ | Host | Guide |
65
+ |------|--------|
66
+ | **OpenCode 1.x** | [docs/opencode-1.md](docs/opencode-1.md) — classic `plugin` plus optional 1.18 `plugin/v2` |
67
+ | **OpenCode 2.0** | [docs/opencode-2.md](docs/opencode-2.md) — `plugin/opencode2` only, including [safe transition](docs/opencode-2.md#safe-transition) off `ctx.catalog` and the deprecated `opencode.json` catalog dump |
68
+ | **Other agents** | [Coding-agent compatibility](#coding-agent-compatibility) |
69
+
70
+ Do not load the 1.x and 2.0 entrypoints in the same host.
71
+
72
+ OpenCode 1.x (`~/.config/opencode/opencode.json`):
65
73
 
66
74
  ```json
67
75
  {
68
76
  "$schema": "https://opencode.ai/config.json",
69
- "plugin": ["cursor-opencode-provider"],
70
- "provider": {
71
- "cursor": {
72
- "npm": "cursor-opencode-provider",
73
- "name": "Cursor",
74
- "models": {}
75
- }
76
- }
77
+ "plugin": ["cursor-opencode-provider"]
77
78
  }
78
79
  ```
79
80
 
80
- Pin a version if you want: `"cursor-opencode-provider@0.4.1"`.
81
+ OpenCode 2.0 (prefer a dedicated `OPENCODE_CONFIG_DIR`):
81
82
 
82
- You can also install it yourself first:
83
-
84
- ```bash
85
- npm install cursor-opencode-provider
86
- # or: bun add cursor-opencode-provider
83
+ ```json
84
+ {
85
+ "plugin": ["cursor-opencode-provider/plugin/opencode2"]
86
+ }
87
87
  ```
88
88
 
89
- ### From a local clone
89
+ Local clones, 1.18 `plugin/v2`, config isolation, and leftover `providers.cursor` cleanup are in the host guides. Shared catalog behavior (variants, 1M, Fast, images, Max Mode) is under [Select a model](#select-a-model).
90
90
 
91
91
  ```bash
92
92
  git clone https://github.com/oakimov/cursor-opencode-provider.git
@@ -95,148 +95,20 @@ bun install
95
95
  bun run build
96
96
  ```
97
97
 
98
- Point classic OpenCode config at the built files with absolute `file://` URLs:
99
-
100
- ```json
101
- {
102
- "plugin": ["file:///absolute/path/to/cursor-opencode-provider/dist/plugin.js"],
103
- "provider": {
104
- "cursor": {
105
- "npm": "file:///absolute/path/to/cursor-opencode-provider/dist/index.js",
106
- "name": "Cursor",
107
- "models": {}
108
- }
109
- }
110
- }
111
- ```
112
-
113
- For **OpenCode 2.0** local development, prefer the plugin path plus `CURSOR_OPENCODE2_DEV_ENTRY` — see [OpenCode 2.0 beta](#opencode-20-beta-opencode2).
98
+ You can also install from npm (`npm install cursor-opencode-provider` or `bun add cursor-opencode-provider`). Pin a version in host config if you want, for example `cursor-opencode-provider@0.4.1`.
114
99
 
115
100
  ## OpenCode setup
116
101
 
117
- If the `cursor` provider block is omitted, the classic plugin auto-registers it on startup (as **Cursor Integration**) using this package's entry. Model entries come from the local cache, which is filled after auth and again on startup when the cache is empty but credentials remain.
118
-
119
- For OpenCode builds that use the Effect/Promise **v2** plugin API (`plugins` field), also load:
120
-
121
- ```json
122
- {
123
- "plugins": ["cursor-opencode-provider/plugin/v2"]
124
- }
125
- ```
126
-
127
- Local clone equivalent: `"file:///absolute/path/to/cursor-opencode-provider/dist/plugin-v2.js"`.
128
-
129
- That entry registers the provider via `ctx.aisdk.sdk` / `ctx.aisdk.language`. Keep the classic `plugin` entry for auth **and** plugin tools (notably `cursor_image_save` / `custom_websearch`): the 1.18 `/v2/promise` API has no `tool` domain, so a v2-only load silently disables Cursor image generation.
130
-
131
- ### OpenCode 2.0 beta (`opencode2`)
132
-
133
- OpenCode 2.0 uses a different plugin API from the 1.18 `/v2/promise` one above — they
134
- are source-incompatible, so this package ships a **separate** entrypoint for it.
135
- Everything is registered by that one plugin. You do **not** need a `providers` entry,
136
- and there is no `enabled` flag. Do not also load `./plugin/v2` or the classic `plugin`
137
- entry under `opencode2`; they target the older APIs.
138
-
139
- Authenticate with `/connect` inside `opencode2`, choose **Cursor**, then either
140
- browser login or an API key. `CURSOR_API_KEY` is also picked up automatically.
141
-
142
- #### From npm
143
-
144
- ```json
145
- {
146
- "plugins": ["cursor-opencode-provider/plugin/opencode2"]
147
- }
148
- ```
149
-
150
- Pin a version if you want: `"cursor-opencode-provider@0.4.1/plugin/opencode2"`.
151
-
152
- OpenCode 2.0 installs the published package into its host cache and loads the AI SDK
153
- entry from there (`aisdk:cursor-opencode-provider`). No extra env vars are required.
154
-
155
- #### From a local clone (`CURSOR_OPENCODE2_DEV_ENTRY`)
156
-
157
- Loading only the local plugin file is not enough for end-to-end local testing: OpenCode
158
- 2.0's catalog still resolves the AI SDK package through `npm install` into
159
- `<host-cache>/packages/…` unless you override it. Point
160
- `CURSOR_OPENCODE2_DEV_ENTRY` at the built provider entry (`dist/index.js`, which exports
161
- `createCursor`) so the host imports that file directly.
162
-
163
- ```bash
164
- cd /absolute/path/to/cursor-opencode-provider
165
- bun install && bun run build
166
-
167
- export CURSOR_OPENCODE2_DEV_ENTRY=/absolute/path/to/cursor-opencode-provider/dist/index.js
168
- # optional while debugging:
169
- # export CURSOR_PROVIDER_DEBUG=1
170
- ```
171
-
172
- Example OpenCode 2.0 config (for example `~/.config/opencode/opencode.json` or a
173
- project `opencode.json`) that loads the built local plugin:
174
-
175
- ```json
176
- {
177
- "plugins": [
178
- "/absolute/path/to/cursor-opencode-provider/dist/plugin-opencode2.js"
179
- ]
180
- }
181
- ```
182
-
183
- Equivalent forms for the same plugin entry:
184
-
185
- - `"file:///absolute/path/to/cursor-opencode-provider/dist/plugin-opencode2.js"`
186
- - `"../cursor-opencode-provider/dist/plugin-opencode2.js"` (relative to the config file; must start with `./` or `../`)
187
-
188
- You can also drop a symlink into `.opencode/plugins/` (or `~/.config/opencode/plugins/`),
189
- which OpenCode scans automatically:
190
-
191
- ```bash
192
- mkdir -p .opencode/plugins
193
- ln -s /absolute/path/to/cursor-opencode-provider/dist/plugin-opencode2.js \
194
- .opencode/plugins/cursor.js
195
- ```
196
-
197
- Important for local 2.0 development:
198
-
199
- - **Export the env var before starting the daemon.** `opencode2 serve --service` inherits
200
- env only at start time. After changing `CURSOR_OPENCODE2_DEV_ENTRY` or rebuilding
201
- `dist/`, restart the service:
202
-
203
- ```bash
204
- opencode2 service stop
205
- export CURSOR_OPENCODE2_DEV_ENTRY=/absolute/path/to/cursor-opencode-provider/dist/index.js
206
- opencode2 service start
207
- ```
208
-
209
- `opencode2 service set` does **not** accept arbitrary env var names.
210
- - **Unset `CURSOR_OPENCODE2_DEV_ENTRY` in production** so the catalog uses the published
211
- `aisdk:cursor-opencode-provider` package again.
212
- - **OpenCode does not install dependencies for local plugin files.** Keep the clone's
213
- `bun install` intact and load the plugin from inside the clone (a copied-out file will
214
- not resolve `protobufjs`).
215
- - **Rebuild after every change** (`bun run build`). The daemon reads `dist/`, not `src/`.
216
-
217
- Feature parity with the classic plugin, and the two places 2.0 differs:
218
-
219
- | Classic plugin | OpenCode 2.0 plugin |
220
- |---|---|
221
- | `config` hook registers provider + models | `ctx.catalog.transform` |
222
- | `auth` hook (OAuth + API key) | `ctx.integration.transform` + `/connect` |
223
- | `tool` hook (`custom_websearch`) | `ctx.tool.transform` |
224
- | `event` hook (session activity) | `ctx.event.subscribe` |
225
- | `tool.execute.before` / `.after` | `ctx.tool.hook(...)` |
226
- | `shell.env` injects the timeout wrapper | **No `shell.env` in 2.0** — the bash command is rewritten to call the wrapper file instead (same behavior, different mechanism) |
227
- | `chat.params` flags compaction turns | **No `chat.params` in 2.0** — the compaction agent is detected in `ctx.session.hook("context")` and correlated by session id |
228
-
229
- > **Beta.** OpenCode 2.0 and its plugin API are both beta and still changing. This
230
- > entrypoint is built against `@opencode-ai/plugin@next` (`0.0.0-next-17155`) and is
231
- > guarded by a compile-time conformance check, but expect churn.
102
+ Host config, auth commands, and local-clone wiring differ by major version — follow [OpenCode 1.x](docs/opencode-1.md) or [OpenCode 2.0](docs/opencode-2.md).
232
103
 
233
104
  ### Authenticate
234
105
 
235
- ```bash
236
- opencode auth login
237
- ```
106
+ | Host | How |
107
+ |------|-----|
108
+ | **OpenCode 1.x** | `opencode auth login` → **cursor** ([details](docs/opencode-1.md#authenticate)) |
109
+ | **OpenCode 2.0** | `/connect` → **Cursor** ([details](docs/opencode-2.md#authenticate)). `CURSOR_API_KEY` is also picked up automatically. |
238
110
 
239
- Choose the **cursor** provider, then one of:
111
+ Then choose a method:
240
112
 
241
113
  | Method | Description |
242
114
  |--------|-------------|
@@ -359,12 +231,9 @@ Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `crs
359
231
  | `CURSOR_GET_SERVER_CONFIG_TELEMETRY` | Set to `1` or `true` to opt the `GetServerConfig` lookup into telemetry in OpenCode/plugin usage |
360
232
  | `CURSOR_PROVIDER_DEBUG` | Set to `1` or `true` to enable wire-level debug logging |
361
233
  | `CURSOR_PROVIDER_DEBUG_FILE` | Debug log path (default: `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) |
362
- | `CURSOR_OPENCODE2_DEV_ENTRY` | **Local OpenCode 2.0 only.** Absolute path to a built entry file (usually `dist/index.js`). Rewrites the catalog's AI SDK package to `aisdk:file://…` so the daemon imports your local build instead of `npm install`-ing the published package. Export it **before** `opencode2 service start`, then restart after rebuilds. Unset in production. |
363
- | `XDG_CACHE_HOME` | Base for host cache dirs (`$XDG_CACHE_HOME/opencode/`, `…/mimocode/`, `…/kilo/`, or OMP's migrated `…/omp/`) when no explicit `cacheDir` / OCP detect override |
364
- | `PI_CODING_AGENT_DIR` | Custom Pi/OMP agent root; provider stores its private cache below `<agent-root>/cache/cursor-opencode/` |
365
- | `PI_CONFIG_DIR` | Custom Pi/OMP config root name used when deriving the default agent root |
366
- | `MIMOCODE_HOME` | When set, host cache is `$MIMOCODE_HOME/cache` (MiMo) |
367
- | `KILO_CONFIG_DIR` | When set, host cache is `$XDG_CACHE_HOME/kilo` |
234
+ | `CURSOR_OPENCODE2_DEV_ENTRY` | **Local OpenCode 2.0 only.** Absolute path to a built entry file (usually `dist/index.js`). Rewrites the AI SDK package to `aisdk:file://…` so the daemon imports your local build instead of `npm install`-ing the published package. Export it **before** `opencode2 service start`, then restart after rebuilds. Unset in production. See [OpenCode 2.0 local clone](docs/opencode-2.md#from-a-local-clone-cursor_opencode2_dev_entry). |
235
+ | `CURSOR_OPENCODE2_TODOS` | **OpenCode 2.0 only.** Set to `1` or `true` to register plugin-owned `todowrite`/`todoread` (in-memory; no TUI sidebar). Off by default. OpenCode 1.x still uses the host builtin. |
236
+ | `XDG_CACHE_HOME` | Base for native OpenCode cache (`$XDG_CACHE_HOME/opencode/`) when no explicit `cacheDir` or structural host path bridge is installed |
368
237
  | `XDG_DATA_HOME` | When set, OpenCode `auth.json` is read from `$XDG_DATA_HOME/opencode/` instead of `~/.local/share/opencode/` |
369
238
 
370
239
  `createCursor({ agentBaseURL })` overrides the agent Run host. When unset, the provider resolves the host from Cursor's `GetServerConfig` API (`agentUrlConfig.agentnUrl`, region-specific — e.g. `agentn.us.api5.cursor.sh`, `agent-gcpp-uswest.api5.cursor.sh`) once per process and holds it in memory (never written to disk), so a held-open Run stream is never repointed mid-session. Explicit agent overrides and GetServerConfig results are validated as HTTPS `*.cursor.sh` hosts (Cursor's agent hostnames vary and may change); non-`cursor.sh` hosts are rejected. Shared HTTP/2 connections are rotated before they become server-aged, while existing Runs may finish on their original connection. The lookup sends `{ "telem_enabled": false }` by default; set `telemetryEnabled: true` in provider config, or `CURSOR_GET_SERVER_CONFIG_TELEMETRY=1` for OpenCode/plugin usage, to opt in. If the lookup fails or does not return a valid Cursor agent host, the model call fails clearly instead of falling back to `agentn.global.api5.cursor.sh`.
@@ -399,8 +268,8 @@ OpenCode
399
268
  |--------|------|
400
269
  | `src/plugin.ts` | Classic OpenCode hooks: provider registration, OAuth, API key exchange, token refresh |
401
270
  | `src/plugin-v2.ts` | OpenCode 1.18 Effect/Promise v2 plugin (`ctx.aisdk.*`); load via `./plugin/v2` only |
402
- | `src/plugin-opencode2.ts` | OpenCode 2.0 beta plugin (catalog, integration, tools, aisdk); load via `./plugin/opencode2` only |
403
- | `src/opencode2/` | 2.0-only catalog mapping, integration/auth, and local API types |
271
+ | `src/plugin-opencode2.ts` | OpenCode 2.0 plugin (`provider.transform`, integration, tools, aisdk, `shell.create.before`, `websearch`, plan kickoff); load via `./plugin/opencode2` or `./server` |
272
+ | `src/opencode2/` | 2.0-only provider inventory, integration/auth, and local API types |
404
273
  | `src/plugin-core.ts` | Host-neutral SDK factory, package matching, API base/telemetry resolution |
405
274
  | `src/model-config.ts` | Cursor model → OpenCode model mapping shared by every plugin surface |
406
275
  | `src/image-input.ts` | AI SDK image attachment validation, decoding, and size limits |
@@ -419,9 +288,9 @@ OpenCode
419
288
 
420
289
  ### Injected system guidance
421
290
 
422
- The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite without inviting catalog-comparison narration, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
291
+ The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite when those tools are advertised without inviting catalog-comparison narration, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
423
292
 
424
- Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected.
293
+ Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. OpenCode 2 dropped the host builtins; set `CURSOR_OPENCODE2_TODOS=1` (or `true`) to register the same canonical names when the catalog omits them (in-memory per session, cleared on `session.deleted` and process restart). With the flag unset, the plugin registers no todo tools. Enabled plugin tools are **direct** catalog tools (`options.codemode: false` plus an `output` schema), not Code Mode helpers. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected; a provider process restart clears it until the next write/read (intentional — see `tasks/lessons.md`).
425
294
 
426
295
  If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`src/language-model.ts`](src/language-model.ts) and its focused coverage in [`test/prompt-history.test.ts`](test/prompt-history.test.ts).
427
296
 
@@ -443,15 +312,14 @@ OpenCode 2.0 does not perform this substitution, so the path is 1.x-only in prac
443
312
 
444
313
  ### Native subagent routing
445
314
 
446
- The provider reads the permission-filtered subagent catalog from the current host `task` or `actor` definition and advertises those recipients to Cursor as custom subagents. Exact configured names are preserved; unknown future Cursor subtype strings fall back to `general`. If a complete catalog omits every compatible recipient, the request fails on Cursor's typed subagent channel instead of selecting an arbitrary specialist.
315
+ The provider reads the permission-filtered subagent catalog from the current host `task` or `subagent` definition and advertises those recipients to Cursor as custom subagents. Native Cursor Task exec remaps onto `task` when advertised, otherwise onto OpenCode 2 `subagent` (`agent` / `sessionID` / `background`). Exact configured names are preserved; unknown future Cursor subtype strings fall back to `general`. If a complete catalog omits every compatible recipient, the request fails on Cursor's typed subagent channel instead of selecting an arbitrary specialist.
447
316
 
448
- | Host | Default recipients | Optional recipients |
449
- |------|--------------------|---------------------|
450
- | OpenCode | `general`, `explore` | Add `agent/*.md` or `agents/*.md` with `mode: subagent` or `all` |
451
- | Kilo | `general`, `explore` | Enable `scout` with `KILO_EXPERIMENTAL_SCOUT=true` (or `KILO_EXPERIMENTAL=true`); add `.kilo/agent/*.md` custom agents with `mode: subagent` or `all` |
452
- | MiMo | `general`, `explore` | Add `.mimocode/agent/*.md` with `mode: subagent` |
317
+ | Host | Executor | Recipients |
318
+ |------|----------|------------|
319
+ | OpenCode 1.x | `task` | Permission-filtered builtins such as `general` / `explore`, plus configured subagents |
320
+ | OpenCode 2.0 | `subagent` | The permission-filtered names listed by the host's tool definition |
453
321
 
454
- Kilo `scout` is reserved for external documentation, dependency repositories, and upstream source. Local workspace discovery continues to use `explore`. Primary modes and hidden/internal agents are never selected unless the host explicitly exposes them as spawnable.
322
+ Primary modes and hidden/internal agents are never selected unless OpenCode explicitly exposes them as spawnable. Alternate-host agents and schemas are OCP responsibilities; see its host guides under [Coding-agent compatibility](#coding-agent-compatibility).
455
323
 
456
324
  ## Package exports
457
325
 
@@ -460,7 +328,8 @@ Kilo `scout` is reserved for external documentation, dependency repositories, an
460
328
  | `cursor-opencode-provider` | `createCursor`, `CursorPlugin` (named + default) |
461
329
  | `cursor-opencode-provider/plugin` | `CursorPlugin` (classic Hooks — auth) |
462
330
  | `cursor-opencode-provider/plugin/v2` | OpenCode 1.18 Effect/Promise v2 plugin (`ctx.aisdk.*`) |
463
- | `cursor-opencode-provider/plugin/opencode2` | OpenCode 2.0 beta plugin (self-registering: catalog + auth + tools) |
331
+ | `cursor-opencode-provider/plugin/opencode2` | OpenCode 2.0 plugin (self-registering: models + auth + tools) |
332
+ | `cursor-opencode-provider/server` | Same OpenCode 2.0 entry; OpenCode 2 Host.resolve and OpenCode 1.18 `exports["./server"]` |
464
333
  | `cursor-opencode-provider/errors` | Structured provider error classes |
465
334
  | `cursor-opencode-provider/image-save` | Host-neutral `executeCursorImageSave` (pi-bridge / non-plugin hosts) |
466
335
 
@@ -470,11 +339,12 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
470
339
 
471
340
  | Problem | What to try |
472
341
  |---------|-------------|
473
- | No Cursor models in the picker | Confirm Cursor auth (`opencode auth login` → **cursor**, or `/connect` in `opencode2`). Restart OpenCode — if auth is present and the cache is empty, models are fetched on startup. Confirm `provider.cursor.npm` is the package name (or a built `file://…/dist/index.js`). |
342
+ | No Cursor models in the picker (OpenCode 1.x) | Confirm Cursor auth (`opencode auth login` → **cursor**). Restart OpenCode — if auth is present and the cache is empty, models are fetched on startup. Confirm `provider.cursor.npm` is the package name (or a built `file://…/dist/index.js`). See [OpenCode 1.x troubleshooting](docs/opencode-1.md#troubleshooting). |
343
+ | No Cursor models in the picker (`opencode2`) | Confirm `/connect` → **Cursor** (or shared `auth.json`). Use a dedicated `OPENCODE_CONFIG_DIR` (not mixed with 1.x). Ensure `$OPENCODE_CONFIG_DIR/plugins/cursor/` is a package directory re-exporting `plugin/opencode2` (not a bare `.js`). After auth, Cursor models appear in the picker from the in-memory inventory — no `opencode.json` model list is required. Filter by provider **Cursor** (`time.released` is `0`, so models sort last). Leftover `providers.cursor` fights that inventory — [safe transition](docs/opencode-2.md#safe-transition). |
474
344
  | Auth / 401 errors mid-session | Re-login. OAuth and exchanged API-key JWTs refresh automatically when near expiry; a revoked refresh token needs a fresh login. |
475
- | Local OpenCode 2.0 still runs the published package | Set `CURSOR_OPENCODE2_DEV_ENTRY` to an absolute `…/dist/index.js` path **before** starting the daemon, rebuild (`bun run build`), then `opencode2 service stop && opencode2 service start`. Loading only `dist/plugin-opencode2.js` is not enough — without the env var, 2.0 still `npm install`s the published package into the host cache. |
345
+ | Local OpenCode 2.0 still runs the published package | Set `CURSOR_OPENCODE2_DEV_ENTRY` to an absolute `…/dist/index.js` path **before** starting the daemon, persist it with `opencode2 service set env CURSOR_OPENCODE2_DEV_ENTRY …`, rebuild (`bun run build`), then `opencode2 service restart`. Loading only `dist/plugin-opencode2.js` is not enough — without the env var, 2.0 still `npm install`s the published package into the host cache. |
476
346
  | “Too many connections from different devices” | Device IDs are derived from stable OS identifiers (same approach as the Cursor CLI). Avoid running multiple clients that invent different machine fingerprints for the same account. |
477
- | Empty or stale model list | Delete `<host-cache>/cursor-models.json` (default `~/.cache/opencode/`, or MiMo/Kilo host cache) and restart OpenCode. Existing Cursor auth is enough to refill the cache; re-login only if auth itself is broken. Cache TTL is 24h; a failed background refresh keeps serving the previous cache. |
347
+ | Empty or stale model list | Delete `<host-cache>/cursor-models.json` (native default `~/.cache/opencode/`) and restart OpenCode. A compatibility layer may supply a different host-cache root through the structural path bridge. Existing Cursor auth is enough to refill the cache; re-login only if auth itself is broken. Cache TTL is 24h; a failed background refresh keeps serving the previous cache. |
478
348
  | Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections, resumes transient interruptions from the latest eligible Cursor checkpoint, and falls back to a fresh-history rebase only before stateful output when no checkpoint exists. Repeated interruption is surfaced as an error instead of a false successful stop; retry the turn after checking connectivity. With debug logging enabled, look for `Run interrupted`, `resuming … checkpoint`, or `rebasing fresh Run`. Restart OpenCode after rebuilding a local `file://` install. |
479
349
  | No response / silent 200 + close | HTTP 200 alone is not a successful agent turn: the provider now requires Cursor's explicit `turn_ended`, captures HTTP/2 trailers/GOAWAY, and recovers once from bare EOF. The Run host still comes from in-memory `GetServerConfig` resolution; set `CURSOR_PROVIDER_DEBUG=1` to confirm the host and termination reason. |
480
350
  | Visible `<shell_metadata>` timeout text | Rebuild and restart a local install. Cursor's shell timeout is carried on its exec request; the provider now removes OpenCode's internal timeout envelope before it is rendered or stored, then returns Cursor's typed timeout or background-handoff event instead of treating the text as successful stdout. |
@@ -497,16 +367,16 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
497
367
  - **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` → `todowrite`, `read_todos_tool_call` → `todoread`, `create_plan_tool_call` → `todowrite`); the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name. Known-but-unsupported variants are soft-denied on the held Run with a populated typed error/`rejected`/`allowlisted:false` result (or `throw` when the CLI has no typed error shape) so the turn continues; unknown future request fields still fail the Run explicitly rather than receiving a guessed response that could deadlock it.
498
368
  - **Progress-only fragments can get one continuation** — if a real tool-enabled turn ends with a short progress fragment ("Checking the workspace", "Looking into it now") and no host tool call, the provider opens one follow-up Run with a synthetic user nudge so the model can call a listed tool or finish the answer. Complete answers that merely start with those verbs are not continued. Title/compaction turns (`allowTools=false`) never continue. If the continuation fails, the original `turn_ended` is still reported as a successful stop.
499
369
  - **Tool availability is per OpenCode agent** — Cursor can request native capabilities such as Task even when a child or restricted OpenCode agent did not advertise the corresponding host tool. The provider prompts Cursor with the exact current catalog and checks every decoded host-tool exec request against it. An unavailable request is answered on Cursor's correlated typed result channel and is never emitted as OpenCode's `invalid` tool.
500
- - **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. The plugin registers its search fallback directly as `custom_websearch`, avoiding OpenCode's reserved `websearch` id filter for third-party providers and taking precedence over MCP search providers such as Brave without host environment configuration. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results.
370
+ - **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. OpenCode 2 ships `websearch` as a direct, permission-gated catalog tool; this plugin supplies it an Exa provider through `ctx.websearch.transform` and aliases the host tool for Cursor. It does not add a second direct fallback because OpenCode 2's public plugin tool context cannot raise the host permission prompt. Classic 1.x still registers `custom_websearch` through its permission-aware tool API to avoid OpenCode's reserved `websearch` id filter. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results.
501
371
  - **MCP resource tools (`list_mcp_resources` / `read_mcp_resource`) also use collision-safe aliases, and Cursor's native resource exec is always answered** — OpenCode's resource tools (advertised only when a connected MCP server declares `capabilities.resources`) share bare names with two of Cursor's own native tool-call variants. Left unaliased, Cursor's client can route a call onto its native exec channel instead of the ordinary MCP tool path, which this provider does not translate — confirmed live: reading a resource was dispatched natively and killed the Run before this fix. The provider now aliases both names to `custom_list_mcp_resources` / `custom_read_mcp_resource` (same mechanism as web search/fetch) so normal calls execute through OpenCode's tools, and separately answers Cursor's native resource-exec fields directly on the protocol floor — an empty list, or a "server not found" error for read — so an unsolicited native request from Cursor can never fail the Run. Downloads (`download_path`) have no OpenCode equivalent and are always refused; binary resource content is subject to the same [50 KB / attachment-type limits](#known-limitations) as other tool results.
502
372
  - **Background shells are non-interactive** — Cursor's native background-shell spawn and soft-background shell-stream timeouts are bridged through OpenCode's foreground-only `bash` tool. With bash/zsh, the classic plugin keeps the original permission/UI command and executes the wrapper through `shell.env` (`BASH_ENV` / `ZDOTDIR`); OpenCode's non-interactive sh/dash argv ignores those startup variables, so the plugin uses a short `exec /bin/sh '<wrapper-file>'` command backed by the same private wrapper. Native background-spawn requests also carry a self-contained marker-producing fallback when the classic hooks are absent. Spawn/soft-bg wrappers detach with `nohup`, redirect output under `${TMPDIR:-/tmp}/cursor-opencode-{bg,shell}.*`, and return the real PID (or typed timeout/exit) to Cursor. Private markers and OpenCode's `<shell_metadata>` envelope are stripped before storage/render, with a short still-running / started / timed-out status line left for the bash bubble. Requests that require `write_shell_stdin` are rejected explicitly because OpenCode does not expose an interactive background-process lifecycle through its AI SDK tool interface. The POSIX wrap path is not implemented for native Windows PowerShell/`cmd`.
503
373
  - **Cursor-native AskQuestion reaches you through OpenCode's question tool** — when Cursor asks a clarifying question, the provider translates the interaction into the host `question` tool, so the prompt appears in OpenCode with its real options and your answer is mapped back to Cursor's option ids (an answer you type yourself becomes Cursor's freeform text). Questions Cursor marks as non-blocking are acknowledged immediately and answered later, matching Cursor CLI. If the current agent does not offer `question` — most subagents deny it, and compaction turns have no tools — the request is declined with that reason and the model asks in its reply instead.
504
- - **Cursor-native SwitchMode reaches you through OpenCode plan enter/exit** — when Cursor raises `switch_mode_request_query`, the provider translates it into the host `plan_enter` or `plan_exit` tool (when advertised) and keeps Cursor waiting until you approve or reject, matching Cursor CLI. `plan`/`spec` enter plan mode; every other non-empty target (including `agent`, `build`, `chat`, `debug`, `edit`, `background`, `multitask`, `triage`, `project`, and unknowns) leaves plan via `plan_exit`. After approval, the provider injects a Cursor CLI-shaped `<system_reminder>` for that mode on later turns so the agent gets the same behavioral instructions the CLI would send.
505
- - **Cursor image generation saves through OpenCode's permission** — Cursor generates the image on its servers and then writes it to you like any other file. Because that write carries raw bytes, which OpenCode's text `write` tool cannot represent, the provider holds the bytes and asks OpenCode for the same permissions a normal write uses — the external-directory prompt when the target sits outside your project, then `edit` — naming the target file. Approve it and the image is written byte-for-byte to its path; deny it and nothing reaches disk — Cursor is told the write was denied, using its own permission-denied result, so the model knows the file is not there. Generation is only approved when the save path is available, so Cursor quota is never spent on an image that could not be saved. By default images land in the Cursor project folder under `assets/`, kept out of your git tree as agent artifacts. Ask for a specific path and the image is saved there instead. Editing an existing image with `reference_image_paths` is not supported yet. The approach is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas, which established writing image bytes from inside an OpenCode plugin tool.
374
+ - **Cursor-native SwitchMode follows each OpenCode generation's planning model** — on OpenCode 1.x, advertised `plan_enter` / `plan_exit` tools remain authoritative and Cursor waits for their approve/reject result. OpenCode 2.0 replaced those tools with vendor-maintained primary `plan` / `build` agents, so its plugin applies an approved Cursor mode change through `session.switchAgent` after the owning Run is terminal; leaving plan still uses the advertised `question` gate when no `plan_exit` tool exists. Direct host UI agent switches are also observed. Any real host-agent or stable system-prompt change rotates and reseeds the Cursor conversation instead of resuming an incompatible checkpoint, while title/generation lifecycle prompts remain ephemeral and do not evict the primary cache. Provider-owned Cursor reminders remain the fallback when a host cannot switch agents.
375
+ - **Cursor image generation saves through OpenCode's permission where the host API exposes it** — Cursor generates the image on its servers and then writes it like any other file. On the classic plugin/OCP surfaces, the provider holds raw bytes and asks for the same permissions as a normal write (`external_directory`, then `edit`) before saving byte-for-byte. OpenCode 2.0's public plugin `ToolContext` does not expose a permission-request method, so its entrypoint deliberately does not advertise `cursor_image_save`; generation and binary writes are refused before staging instead of bypassing permissions or exposing an always-failing tool. By default supported-host images land in the Cursor project folder under `assets/`, kept out of the git tree as agent artifacts. Editing an existing image with `reference_image_paths` is not supported yet. The classic implementation is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas.
506
376
  - **Cursor-native CreatePlan writes a host plan file, then asks before executing it** — when Cursor raises `create_plan_request_query`, the provider writes plain markdown to your host's own plans directory (`<hostGlobalDataDir>/plans/`, never your repository and never Cursor's `~/.cursor/plans`), with no Cursor YAML frontmatter. Writing the plan does not ask; running it does. If you are in plan mode, the provider prints the plan into the conversation — Cursor sends it over a side channel that would otherwise never display it — and then asks *"Plan at &lt;path&gt; is complete. Would you like to switch to the build agent and start implementing?"* — the same prompt OpenCode's own `plan_exit` uses — and keeps Cursor waiting for your answer. Answer **Yes** and the plan is reported as approved and implementation begins; answer **No** (or dismiss it) and Cursor is told the plan was not accepted, so the model refines it and proposes again instead of starting work. Hosts that advertise their own plan-staging tool run their native review UI instead, and it reports the same approve/refine outcome. Empty args still get the CLI empty-`plan_uri` success ack. Display `create_plan_tool_call` continues to mirror into `todowrite` separately.
507
377
  - **Other Cursor-native interaction queries remain headless** — remaining UI/approval *queries* (as distinct from display tool calls and the AskQuestion / SwitchMode / CreatePlan / GenerateImage bridges) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects available web capabilities to executable host tools (`custom_websearch`, `custom_webfetch`); native web/PR/MCP/SCM requests are declined so they remain behind host tool permissions. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
508
378
  - **Compaction resets Cursor conversation state without resetting stable workspace context** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Across both id rotations, the frozen workspace/rules/environment base and its prior encoded-context comparison seed are transferred instead of rebuilt; tools, skills, agents, plugins, and MCP state are still rediscovered, and the complete context is reused only when its bytes remain identical. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state. Debug logs include system-prompt and encoded RequestContext hashes for cache-prefix verification.
509
- - **Conversation bindings survive provider restarts** — after each successful `TurnEnded`, the OpenCode-session binding, latest checkpoint, checkpoint-reachable KV blob graph, frozen request-context base, last real tool catalog, and pending post-compaction rebase marker are atomically replaced in that session's private `<host-cache>/cursor-conversations/*.pb.gz` snapshot. The protobuf stores checkpoints, blob ids/data, and request context as bytes without JSON/base64 expansion, then gzip compresses the complete record. A checkpoint alone is insufficient: Cursor requests its referenced hashes through `get_blob` when the next Run starts. Reachability follows Cursor CLI's export traversal (turns, messages, steps, images, summaries, todos, prompts, and nested subagents), pruning superseded blobs; decode failure or a missing referenced hash conservatively retains the complete blob set. The stable OpenCode session id selects the file; Cursor conversation ids may rotate during compaction and remain inside the snapshot. Independent files prevent concurrent OpenCode processes from losing unrelated sessions by replacing a shared aggregate cache. Startup restores records updated within 24 hours and safely flushes older ones without deleting a concurrently published fresh replacement. Experimental legacy JSON snapshots are discarded rather than migrated. Debug logs report hydration as `restored`, `missing`, `invalid`, or `expired`, plus the compressed and protobuf snapshot sizes. The restored catalog is a fallback only for lifecycle turns such as compaction; an ordinary restricted/no-tool turn never resurrects it. Process-global bindings and compaction catalogs still use a 256-session LRU bound; memory eviction drops the live checkpoint, blobs, and frozen context, which can be rehydrated from the restart cache on later use.
379
+ - **Conversation bindings survive provider restarts** — after each successful `TurnEnded`, the OpenCode-session binding, latest checkpoint, checkpoint-reachable KV blob graph, frozen request-context base, last real tool catalog, host agent, stable system-prompt hash, and pending post-compaction rebase marker are atomically replaced in that session's private `<host-cache>/cursor-conversations/*.pb.gz` snapshot. The protobuf stores checkpoints, blob ids/data, and request context as bytes without JSON/base64 expansion, then gzip compresses the complete record. A checkpoint alone is insufficient: Cursor requests its referenced hashes through `get_blob` when the next Run starts. Reachability follows Cursor CLI's export traversal (turns, messages, steps, images, summaries, todos, prompts, and nested subagents), pruning superseded blobs; decode failure or a missing referenced hash conservatively retains the complete blob set. The stable OpenCode session id selects the file; Cursor conversation ids may rotate during compaction, agent switches, or prompt changes and remain inside the snapshot. Independent files prevent concurrent OpenCode processes from losing unrelated sessions by replacing a shared aggregate cache. Startup restores records updated within 24 hours and safely flushes older ones without deleting a concurrently published fresh replacement. Experimental legacy JSON snapshots are discarded rather than migrated. Debug logs report hydration as `restored`, `missing`, `invalid`, or `expired`, plus the compressed and protobuf snapshot sizes. The restored catalog is a fallback only for lifecycle turns such as compaction; an ordinary restricted/no-tool turn never resurrects it. Process-global bindings and prompt/catalog state still use a 256-session LRU bound; memory eviction drops the live checkpoint, blobs, and frozen context, which can be rehydrated from the restart cache on later use.
510
380
  - **Oversized checkpoint graphs rebase instead of flooding the Run stream** — before reusing a checkpoint, the provider measures its reachable KV graph. State above 100 MiB, or an incomplete graph with a missing/undecodable reference, rotates to a fresh Cursor conversation seeded from OpenCode's authoritative history. KV, request-context, MCP-state, interaction, and heartbeat writes are ordered per Run and await HTTP/2 drain, so a large blob reply cannot create an unbounded queue later misreported as heartbeat backpressure. Debug logs distinguish checkpoint-envelope bytes, reachable blob bytes, seed size, and encoded Run-request estimate; raw blob bytes are never presented as tokens.
511
381
  - **Interrupted Runs resume from checkpoints** — a remote EOF, Connect end-stream, or trailer error is never emitted as a successful `stop`. When the failed Run produced an eligible checkpoint, the provider opens a new RPC for the same conversation and sends that state with `ResumeAction`, so completed text and tool work are not replayed. Before any stateful output, an interruption without a checkpoint can still rebase from OpenCode history. Stateful interruptions without a checkpoint are surfaced because replay would be ambiguous; retry exhaustion remains explicit. A transport closure after `turn_ended` is treated as successful completion.
512
382
  - **Large reads are capped at 50 KB by OpenCode, and the cap is reported** — OpenCode's `read` tool stops at `MAX_BYTES = 50 * 1024`, cutting on a whole-line boundary, and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. The provider strips that envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states the cap: a capped native read appends a `[Partial read: …]` marker after the content, a capped MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. The structured `truncated` flag alone is not enough — verified against live `gpt-5.4-mini` and `grok-4.5` sessions, both of which asserted "highly confident" that partial content was the whole file until the textual marker was added. Deliberately paged reads (explicit `offset`/`limit`) are not marked. Whole-file writes and overwriting `Add File` patches that echo the marker are rejected before OpenCode executes them; targeted edits remain possible, including edits to source that quotes the warning text. Cursor's private legacy-edit read is handled separately as described above, so an edit can operate on a workspace file larger than 50 KB without turning the capped preview into a replacement. The cap is hardcoded in OpenCode's read tool — it is not the configurable `tool_output.max_bytes`, which governs `Truncate.Service` and not read's line accumulation — so ordinary model reads still require paging with `offset`.
@@ -4,8 +4,9 @@
4
4
  * The classic plugin learns that a turn belongs to OpenCode's compaction agent
5
5
  * via the `chat.params` hook and forwards it as a provider option. OpenCode 2.0
6
6
  * removed `chat.params`; the only place the runtime names the owning agent is
7
- * `session.hook("context")`, which fires before dispatch. We record the fact
8
- * there and let the provider read it back by session id.
7
+ * `session.hook("context")`, which fires before dispatch. The 2.0 plugin writes
8
+ * an explicit request-local provider option and records this session marker as
9
+ * a compatibility fallback for hosts that do not preserve mutable options.
9
10
  *
10
11
  * Bounded so a long-lived server cannot accumulate ids for dead sessions.
11
12
  */
@@ -4,8 +4,9 @@
4
4
  * The classic plugin learns that a turn belongs to OpenCode's compaction agent
5
5
  * via the `chat.params` hook and forwards it as a provider option. OpenCode 2.0
6
6
  * removed `chat.params`; the only place the runtime names the owning agent is
7
- * `session.hook("context")`, which fires before dispatch. We record the fact
8
- * there and let the provider read it back by session id.
7
+ * `session.hook("context")`, which fires before dispatch. The 2.0 plugin writes
8
+ * an explicit request-local provider option and records this session marker as
9
+ * a compatibility fallback for hosts that do not preserve mutable options.
9
10
  *
10
11
  * Bounded so a long-lived server cannot accumulate ids for dead sessions.
11
12
  */
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Host-owned primary-agent synchronization for Cursor SwitchMode.
3
+ *
4
+ * The language model stays host-neutral: it records a canonical Cursor mode
5
+ * (`plan`, `spec`, `agent`, ...). A host entrypoint may install this structural
6
+ * callback when its public API can select the corresponding primary agent.
7
+ * OpenCode 2.0 uses it to select its vendor-maintained `plan` / `build` agents;
8
+ * OpenCode 1.x continues to use advertised `plan_enter` / `plan_exit` tools.
9
+ */
10
+ export type HostAgentModeSwitchInput = {
11
+ sessionID: string;
12
+ targetModeID: string;
13
+ /** Concrete Cursor Run that owns the switch. */
14
+ cursorSessionID?: string;
15
+ };
16
+ export type HostAgentModeSwitchFn = (input: HostAgentModeSwitchInput) => void | Promise<void>;
17
+ export declare function setHostAgentModeSwitch(fn: HostAgentModeSwitchFn | undefined): void;
18
+ /** Queue only when this host installed a native primary-agent switch. */
19
+ export declare function queueHostAgentModeSwitch(input: HostAgentModeSwitchInput): boolean;
20
+ export declare function cancelHostAgentModeSwitch(sessionID: string | undefined): void;
21
+ /**
22
+ * Apply the switch only after its Cursor Run is terminal and owns no pending
23
+ * execs. This avoids changing the host's permission/catalog state underneath a
24
+ * held Run that still needs to finish or receive a tool result.
25
+ */
26
+ export declare function flushHostAgentModeSwitch(sessionID: string | undefined, options?: {
27
+ cursorSessionID?: string;
28
+ terminal?: boolean;
29
+ pumpActive?: boolean;
30
+ pendingExecs?: number;
31
+ }): Promise<boolean>;
32
+ export declare function resetHostAgentModeSwitchForTests(): void;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Host-owned primary-agent synchronization for Cursor SwitchMode.
3
+ *
4
+ * The language model stays host-neutral: it records a canonical Cursor mode
5
+ * (`plan`, `spec`, `agent`, ...). A host entrypoint may install this structural
6
+ * callback when its public API can select the corresponding primary agent.
7
+ * OpenCode 2.0 uses it to select its vendor-maintained `plan` / `build` agents;
8
+ * OpenCode 1.x continues to use advertised `plan_enter` / `plan_exit` tools.
9
+ */
10
+ import { trace } from "./debug.js";
11
+ let switchHostAgent;
12
+ const pending = new Map();
13
+ const MAX_PENDING_HOST_AGENT_SWITCHES = 256;
14
+ export function setHostAgentModeSwitch(fn) {
15
+ switchHostAgent = fn;
16
+ if (!fn)
17
+ pending.clear();
18
+ }
19
+ /** Queue only when this host installed a native primary-agent switch. */
20
+ export function queueHostAgentModeSwitch(input) {
21
+ const sessionID = input.sessionID.trim();
22
+ const targetModeID = input.targetModeID.trim();
23
+ if (!switchHostAgent || !sessionID || !targetModeID)
24
+ return false;
25
+ pending.set(sessionID, {
26
+ sessionID,
27
+ targetModeID,
28
+ ...(input.cursorSessionID ? { cursorSessionID: input.cursorSessionID } : {}),
29
+ attempts: 0,
30
+ });
31
+ while (pending.size > MAX_PENDING_HOST_AGENT_SWITCHES) {
32
+ const oldest = pending.keys().next().value;
33
+ if (!oldest)
34
+ break;
35
+ pending.delete(oldest);
36
+ }
37
+ trace(`host-agent-mode: pending sessionID=${sessionID} ` +
38
+ `cursorSessionID=${input.cursorSessionID ?? ""} target=${targetModeID}`);
39
+ return true;
40
+ }
41
+ export function cancelHostAgentModeSwitch(sessionID) {
42
+ const key = sessionID?.trim();
43
+ if (key)
44
+ pending.delete(key);
45
+ }
46
+ /**
47
+ * Apply the switch only after its Cursor Run is terminal and owns no pending
48
+ * execs. This avoids changing the host's permission/catalog state underneath a
49
+ * held Run that still needs to finish or receive a tool result.
50
+ */
51
+ export async function flushHostAgentModeSwitch(sessionID, options = {}) {
52
+ const key = sessionID?.trim();
53
+ if (!key)
54
+ return false;
55
+ const state = pending.get(key);
56
+ if (!state || !switchHostAgent)
57
+ return false;
58
+ if (options.terminal !== true || options.pumpActive || (options.pendingExecs ?? 0) > 0) {
59
+ return false;
60
+ }
61
+ if (state.cursorSessionID
62
+ && options.cursorSessionID
63
+ && state.cursorSessionID !== options.cursorSessionID) {
64
+ // A newer Run reached its terminal boundary, so the owner was superseded
65
+ // before its native-agent switch could be applied. Never let that stale
66
+ // request mutate a later turn, and do not retain it indefinitely.
67
+ pending.delete(key);
68
+ trace(`host-agent-mode: discarded stale Run sessionID=${key} ` +
69
+ `owner=${state.cursorSessionID} terminal=${options.cursorSessionID}`);
70
+ return false;
71
+ }
72
+ state.attempts += 1;
73
+ try {
74
+ await switchHostAgent(state);
75
+ pending.delete(key);
76
+ trace(`host-agent-mode: switched sessionID=${key} target=${state.targetModeID}`);
77
+ return true;
78
+ }
79
+ catch (error) {
80
+ // Keep the request pending so a later explicit provider turn can retry at
81
+ // its own terminal boundary. The provider-owned mode reminder remains the
82
+ // behavioral fallback until the native host switch succeeds.
83
+ delete state.cursorSessionID;
84
+ trace(`host-agent-mode: FAILED sessionID=${key} target=${state.targetModeID} ` +
85
+ `attempts=${state.attempts} err=${error instanceof Error ? error.message : String(error)}`);
86
+ return false;
87
+ }
88
+ }
89
+ export function resetHostAgentModeSwitchForTests() {
90
+ switchHostAgent = undefined;
91
+ pending.clear();
92
+ }
@@ -6,6 +6,10 @@ import { type ConversationBlobGraphStats } from "./protocol/blob-store.js";
6
6
  import { type CursorSession } from "./session.js";
7
7
  import { CursorProviderError } from "./errors.js";
8
8
  import type { SeedHistoryMessage } from "./protocol/request.js";
9
+ type PromptIdentity = {
10
+ hostAgent?: string;
11
+ systemPromptHash?: string;
12
+ };
9
13
  export declare const MAX_TURN_STATE_SESSIONS = 256;
10
14
  /** Cursor's own conversation export path treats state above 100 MiB as large. */
11
15
  export declare const MAX_CHECKPOINT_BLOB_GRAPH_BYTES: number;
@@ -149,8 +153,9 @@ export declare function resolveTurnToolState(input: {
149
153
  export declare function resolveTurnConversationReset(input: {
150
154
  sessionKey?: string;
151
155
  isCompaction: boolean;
156
+ promptIdentity?: PromptIdentity;
152
157
  }): {
153
158
  reset: boolean;
154
- reason?: "compaction" | "post-compaction-rebase";
159
+ reason?: "compaction" | "post-compaction-rebase" | "agent-change" | "system-prompt-change";
155
160
  };
156
161
  export declare function resetTurnStateForTests(): void;