| ... | @@ -1,248 +1,260 @@ | ... | @@ -1,248 +1,260 @@ |
| 1 | # Plan: Cursor CLI (`agent`) Provider Integration | 1 | # Plan: Cursor ACP (`agent acp`) Provider Integration |
| 2 | | 2 | |
| 3 | ## Goal | 3 | ## Goal |
| 4 | | 4 | |
| 5 | Add Cursor as a first-class provider in T3 Code using Cursor CLI stream JSON mode (`agent -p --output-format stream-json`), with robust session lifecycle handling and canonical `ProviderRuntimeEvent` projection. | 5 | Add Cursor as a first-class provider in T3 Code using ACP (`agent acp`) over JSON-RPC 2.0 stdio, with robust session lifecycle handling and canonical `ProviderRuntimeEvent` projection. |
| 6 | | 6 | |
| 7 | --- | 7 | --- |
| 8 | | 8 | |
| 9 | ## 1) Exploration Findings (from live CLI runs) | 9 | ## 1) Exploration Findings (from live ACP probes) |
| 10 | | 10 | |
| 11 | ### 1.1 Core invocation shape | 11 | ### 1.1 Core invocation and transport |
| 12 | | 12 | |
| 13 | 1. Binary is `agent` on PATH (`2026.02.27-e7d2ef6` observed). | 13 | 1. Binary is `agent` on PATH (`2026.02.27-e7d2ef6` observed). |
| 14 | 2. Non-interactive streaming mode: | 14 | 2. ACP server command is `agent acp`. |
| 15 | - `agent -p --trust --output-format stream-json --stream-partial-output ...` | 15 | 3. Transport is newline-delimited JSON-RPC 2.0 over stdio. |
| 16 | 3. Session continuity: | 16 | 4. Messages: |
| 17 | - `agent create-chat` returns a chat UUID. | 17 | - client -> server: requests and responses to server-initiated requests |
| 18 | - `--resume <chatId>` and `--continue` preserve history. | 18 | - server -> client: responses, notifications (`session/update`), and server requests (`session/request_permission`) |
| 19 | - Stream `session_id` equals chat UUID. | 19 | |
| 20 | | 20 | ### 1.2 Handshake and session calls observed |
| 21 | ### 1.2 Stream JSON event families observed | 21 | |
| 22 | | 22 | 1. `initialize` returns: |
| 23 | 1. `system/init` | 23 | - `protocolVersion` |
| 24 | 2. `user` | 24 | - `agentCapabilities` (`loadSession`, `mcpCapabilities`, `promptCapabilities`) |
| 25 | 3. `thinking/delta`, `thinking/completed` | 25 | - `authMethods` (includes `cursor_login`) |
| 26 | 4. `assistant` (partial and final) | 26 | 2. `authenticate { methodId: "cursor_login" }` returns `{}` when logged in. |
| 27 | 5. `tool_call/started`, `tool_call/completed` | 27 | 3. `session/new` returns: |
| 28 | 6. `result/success` (`is_error: false` in all successful process exits) | 28 | - `sessionId` |
| 29 | 7. Previously observed under load/rate-limit: | 29 | - `modes` (`agent`, `plan`, `ask`) |
| 30 | - `connection/reconnecting`, `connection/reconnected` | 30 | 4. `session/load` works and requires `sessionId`, `cwd`, `mcpServers`. |
| 31 | - `retry/starting`, `retry/resuming` | 31 | 5. `session/prompt` returns terminal response `{ stopReason: "end_turn" | "cancelled" }`. |
| 32 | - followed by non-JSON terminal line `v: [resource_exhausted] Error` and process exit code `1` | 32 | |
| 33 | | 33 | Important sequence note: |
| 34 | ### 1.3 Tool call variants observed | 34 | 1. ACP currently allows `session/new` even without explicit `initialize`/`authenticate` when local auth already exists. |
| 35 | | 35 | 2. For adapter consistency and forward compatibility, we should still send `initialize` and `authenticate` during startup. |
| 36 | 1. `shellToolCall` | 36 | |
| 37 | 2. `readToolCall` | 37 | ### 1.3 `session/update` event families observed |
| 38 | 3. `editToolCall` | 38 | |
| 39 | 4. `grepToolCall` | 39 | Observed `params.update.sessionUpdate` values: |
| 40 | 5. `globToolCall` | 40 | |
| 41 | | 41 | 1. `available_commands_update` |
| 42 | Tool completion outcomes observed: | 42 | 2. `agent_thought_chunk` |
| 43 | | 43 | 3. `agent_message_chunk` |
| 44 | 1. `result.success` | 44 | 4. `tool_call` |
| 45 | 2. `result.failure` (non-zero exit, stderr) | 45 | 5. `tool_call_update` |
| 46 | 3. `result.rejected` (permission denied by Cursor approval layer) | 46 | |
| 47 | | 47 | Observed payload behavior: |
| 48 | ### 1.4 Critical protocol quirks | 48 | |
| 49 | | 49 | 1. `agent_*_chunk` provides `content: { type: "text", text: string }`. |
| 50 | 1. `tool_call.call_id` contains embedded newline characters (must sanitize for IDs). | 50 | 2. `tool_call` may be emitted multiple times for same `toolCallId`: |
| 51 | 2. Startup failures are plain text, not JSON: | 51 | - initial generic form (`title: "Terminal"`, `rawInput: {}`) |
| 52 | - invalid model -> plain error + exit `1` | 52 | - enriched form (`title: "\`pwd\`"`, `rawInput: { command: "pwd" }`) |
| 53 | - invalid API key -> plain warning + exit `1` | 53 | 3. `tool_call_update` statuses observed: |
| 54 | 3. `result.subtype` can be `success` even when tool calls failed/rejected inside the turn. | 54 | - `in_progress` |
| 55 | 4. Running multiple `agent` commands concurrently can race on `~/.cursor/cli-config.json` writes (`ENOENT rename ...cli-config.json.tmp` observed). | 55 | - `completed` |
| 56 | | 56 | 4. `tool_call_update` on completion may include `rawOutput`: |
| 57 | Fixtures recorded in repo: | 57 | - terminal: `{ exitCode, stdout, stderr }` |
| 58 | | 58 | - search/find: `{ totalFiles, truncated }` |
| 59 | 1. `.tmp/cursor-observations/*.ndjson` | 59 | |
| 60 | 2. `.tmp/cursor-observations/startup-invalid-*.txt` | 60 | ### 1.4 Permission flow observed |
| | 61 | |
| | 62 | 1. ACP server sends `session/request_permission` (JSON-RPC request with `id`). |
| | 63 | 2. Request shape includes: |
| | 64 | - `params.sessionId` |
| | 65 | - `params.toolCall` |
| | 66 | - `params.options` (`allow-once`, `allow-always`, `reject-once`) |
| | 67 | 3. Client must respond on same `id` with: |
| | 68 | - `{ outcome: { outcome: "selected", optionId: "<one-option-id>" } }` |
| | 69 | 4. Reject path still results in tool lifecycle completion events (`tool_call_update status: completed`), typically without `rawOutput`. |
| | 70 | |
| | 71 | ### 1.5 Error and capability quirks |
| | 72 | |
| | 73 | 1. `session/cancel` currently returns: |
| | 74 | - JSON-RPC error `-32601` Method not found |
| | 75 | 2. Error shape examples: |
| | 76 | - unknown auth method: `-32602` |
| | 77 | - `session/load` missing/invalid params: `-32602` |
| | 78 | - `session/prompt` unknown session: `-32603` with details |
| | 79 | 3. Parallel prompts on same session are effectively single-flight: |
| | 80 | - second prompt can cause first to complete with `stopReason: "cancelled"`. |
| | 81 | 4. `session/new` accepts a `model` field (no explicit echo in response). |
| | 82 | |
| | 83 | Probe artifacts: |
| | 84 | 1. `.tmp/acp-probe/*/transcript.ndjson` |
| | 85 | 2. `.tmp/acp-probe/*/summary.json` |
| | 86 | 3. `scripts/cursor-acp-probe.mjs` |
| 61 | | 87 | |
| 62 | --- | 88 | --- |
| 63 | | 89 | |
| 64 | ## 2) Integration Constraints for T3 | 90 | ## 2) Integration Constraints for T3 |
| 65 | | 91 | |
| 66 | 1. T3 requires adapter contract support for: | 92 | 1. T3 adapter contract still requires: |
| 67 | - `startSession`, `sendTurn`, `interruptTurn`, `respondToRequest`, `readThread`, `rollbackThread`, `stopSession`, `listSessions`, `hasSession`, `stopAll`, `streamEvents`. | 93 | - `startSession`, `sendTurn`, `interruptTurn`, `respondToRequest`, `readThread`, `rollbackThread`, `stopSession`, `listSessions`, `hasSession`, `stopAll`, `streamEvents`. |
| 68 | 2. Orchestration depends on canonical runtime events (`ProviderRuntimeEvent`) only. | 94 | 2. Orchestration consumes canonical `ProviderRuntimeEvent` only. |
| 69 | 3. Current `ProviderCommandReactor` ignores `thread.turn-start-requested.payload.provider` and prefers existing provider/session; this must be fixed for reliable provider selection (including Cursor). | 95 | 3. `ProviderCommandReactor` provider precedence fix remains required (respect explicit provider on turn start). |
| 70 | 4. Cursor CLI currently has no external approval callback API; approvals are internal to CLI behavior. | 96 | 4. ACP now supports external permission decisions, so Cursor can participate in T3 approval UX via adapter-managed request/response plumbing. |
| 71 | | 97 | |
| 72 | --- | 98 | --- |
| 73 | | 99 | |
| 74 | ## 3) Proposed Architecture | 100 | ## 3) Proposed Architecture |
| 75 | | 101 | |
| 76 | ## 3.1 New server components | 102 | ### 3.1 New server components |
| 77 | | 103 | |
| 78 | 1. `apps/server/src/provider/Services/CursorAdapter.ts` (service contract/tag only for DI parity) | 104 | 1. `apps/server/src/provider/Services/CursorAdapter.ts` (service contract/tag + ACP event schemas). |
| 79 | 2. `apps/server/src/provider/Layers/CursorAdapter.ts` (single implementation unit; owns Cursor process lifecycle + stream parsing + runtime projection) | 105 | 2. `apps/server/src/provider/Layers/CursorAdapter.ts` (single implementation unit; owns ACP process lifecycle, JSON-RPC routing, runtime projection). |
| 80 | 3. No separate `cursorCliManager.ts` abstraction in v1. | 106 | 3. No manager indirection; keep logic in layer implementation. |
| 81 | | 107 | |
| 82 | ### 3.2 Session model | 108 | ### 3.2 Session model |
| 83 | | 109 | |
| 84 | 1. T3 `ProviderSessionId` = synthetic UUID managed by adapter (stable for T3 APIs). | 110 | 1. One long-lived ACP child process per T3 Cursor provider session. |
| 85 | 2. Cursor chat UUID stored as: | 111 | 2. Track: |
| 86 | - `ProviderSession.resumeCursor = { chatId: string }` | 112 | - `providerSessionId` (T3 synthetic ID) |
| 87 | - `ProviderSession.threadId = ProviderThreadId(chatId)` for adapter contract compatibility. | 113 | - `acpSessionId` (from `session/new` or restored via `session/load`) |
| 88 | 3. Adapter keeps runtime map: | 114 | - `cwd`, `model`, in-flight turn state |
| 89 | - `sessionId -> { chatId, cwd, model, activeChildProcess?, activeTurnId?, turnLog }` | 115 | - pending permission requests by JSON-RPC request id |
| | 116 | 3. Resume support: |
| | 117 | - persist `acpSessionId` in provider resume metadata and call `session/load` on reattach. |
| 90 | | 118 | |
| 91 | ### 3.3 Command strategy | 119 | ### 3.3 Command strategy |
| 92 | | 120 | |
| 93 | 1. `startSession`: | 121 | 1. `startSession`: |
| 94 | - call `agent create-chat` unless `resumeCursor.chatId` exists. | 122 | - spawn `agent acp` |
| 95 | - validate provider is `cursor` when provided. | 123 | - `initialize` |
| | 124 | - `authenticate(cursor_login)` (best-effort, typed failure handling) |
| | 125 | - `session/new` or `session/load` |
| 96 | 2. `sendTurn`: | 126 | 2. `sendTurn`: |
| 97 | - spawn `agent -p --trust --output-format stream-json --stream-partial-output --resume <chatId>`. | 127 | - send `session/prompt { sessionId, prompt: [...] }` |
| 98 | - add `--model` when provided. | 128 | - consume streaming `session/update` notifications until terminal prompt response |
| 99 | - map runtime mode: | | |
| 100 | - `approvalPolicy = never` -> include `--force` | | |
| 101 | - otherwise omit `--force` (commands may be rejected by CLI). | | |
| 102 | - use `--workspace <cwd>` when available. | | |
| 103 | 3. `interruptTurn`: | 129 | 3. `interruptTurn`: |
| 104 | - terminate active child process for session (`SIGINT` then hard kill fallback). | 130 | - no native `session/cancel` today; implement fallback: |
| | 131 | - terminate ACP process + restart + `session/load` for subsequent turns |
| | 132 | - mark in-flight turn as interrupted/failed in canonical events |
| | 133 | 4. `respondToRequest`: |
| | 134 | - map T3 approval decision -> ACP `optionId` |
| | 135 | - reply to exact JSON-RPC request id from `session/request_permission` |
| 105 | | 136 | |
| 106 | ### 3.4 Effect-first implementation style (required) | 137 | ### 3.4 Effect-first implementation style (required) |
| 107 | | 138 | |
| 108 | 1. Keep adapter logic inside `CursorAdapterLive` layer constructor (no manager indirection). | 139 | 1. Keep logic inside `CursorAdapterLive`. |
| 109 | 2. Use Effect process primitives first: | 140 | 2. Use Effect primitives: |
| 110 | - `ChildProcessSpawner` + `ChildProcess.make` from `effect/unstable/process` for `agent` execution. | 141 | - `Queue` + `Stream.fromQueue` for event fan-out |
| 111 | 3. Use Effect concurrency/state primitives: | 142 | - `Ref` / `Ref.Synchronized` for session/process/request state |
| 112 | - `Queue` for adapter event queue, `Stream.fromQueue` for `streamEvents` | 143 | - scoped fibers for stdout/stderr read loops |
| 113 | - `Ref` / `Ref.Synchronized` for session maps and active-turn process handles | 144 | 3. Typed JSON decode at boundary: |
| 114 | - `Effect.scoped` + `Effect.forkScoped` for worker fibers and cleanup | 145 | - request/response envelopes |
| 115 | 4. Use Effect stream parsing path for stdout/stderr: | 146 | - `session/update` union schema |
| 116 | - decode bytes -> line buffer -> JSON parse -> typed projection | 147 | - permission-request schema |
| 117 | - keep non-JSON lines on a fallback branch that emits `runtime.error` | 148 | 4. Keep adapter errors in typed error algebra with explicit mapping at process/protocol boundaries. |
| 118 | 5. Keep errors in typed adapter error algebra (`ProviderAdapter*Error`) via `Effect.mapError` boundaries, not ad-hoc exceptions. | | |
| 119 | | 149 | |
| 120 | --- | 150 | --- |
| 121 | | 151 | |
| 122 | ## 4) Canonical Event Mapping Plan | 152 | ## 4) Canonical Event Mapping Plan (ACP -> ProviderRuntimeEvent) |
| 123 | | 153 | |
| 124 | For each parsed NDJSON line, emit zero or more `ProviderRuntimeEvent`s: | 154 | 1. `session/update: agent_message_chunk` |
| 125 | | 155 | - emit `message.delta` for assistant stream |
| 126 | 1. `system/init` | 156 | 2. prompt terminal response (`session/prompt` result `stopReason: end_turn`) |
| 127 | - emit `session.started` (once per adapter session lifecycle) | 157 | - emit `message.completed` + `turn.completed` |
| 128 | - emit `thread.started` if thread not yet emitted | 158 | 3. `session/update: agent_thought_chunk` |
| 129 | 2. `assistant` partial chunks | 159 | - initial mapping: emit thinking activity (or ignore if we keep current canonical surface minimal) |
| 130 | - emit `message.delta` with stable per-turn synthetic `itemId` | 160 | 4. `session/update: tool_call` |
| 131 | 3. final `assistant` message | 161 | - first-seen `toolCallId` emits `tool.started` |
| 132 | - emit `message.completed` | 162 | - subsequent `tool_call` for same ID treated as metadata update (no duplicate started event) |
| 133 | 4. `tool_call/started` | 163 | 5. `session/update: tool_call_update` |
| 134 | - emit `tool.started` | 164 | - `in_progress`: optional progress activity |
| 135 | - map kind: | 165 | - `completed`: emit `tool.completed` with summarized `rawOutput` when present |
| 136 | - shell -> `command` | 166 | 6. `session/request_permission` |
| 137 | - edit -> `file-change` | 167 | - emit `approval.requested` with mapped options |
| 138 | - read/grep/glob -> `other` | 168 | - when client decision sent, emit `approval.resolved` |
| 139 | 5. `tool_call/completed` | 169 | 7. protocol/process error |
| 140 | - emit `tool.completed` | | |
| 141 | - include summary detail from nested payload (`command`, `path`, stderr fragment, rejection reason) | | |
| 142 | 6. `thinking/*` | | |
| 143 | - initial version: ignore for canonical events (optional later: map to `tool.*` “Thinking”) | | |
| 144 | 7. `result/success` | | |
| 145 | - emit `turn.completed` with `status: completed` unless unrecoverable parse/process error already set failure | | |
| 146 | 8. non-JSON stdout/stderr protocol line | | |
| 147 | - emit `runtime.error` | 170 | - emit `runtime.error` |
| 148 | - emit `turn.completed` with `status: failed` | 171 | - fail active turn/session as appropriate |
| 149 | 9. `connection/*` + `retry/*` (when present) | | |
| 150 | - emit `tool.started/completed` with `toolKind: other`, title like “Connection retry” | | |
| 151 | - if final failure follows, also emit `runtime.error` | | |
| 152 | | 172 | |
| 153 | Synthetic IDs: | 173 | Synthetic IDs: |
| 154 | | 174 | 1. `turnId`: T3-generated UUID per `sendTurn`. |
| 155 | 1. `turnId`: adapter-generated UUID per `sendTurn`. | 175 | 2. `itemId`: |
| 156 | 2. `itemId`: `${turnId}:assistant` for assistant stream; `${turnId}:${sanitizedCallId}` for tool calls. | 176 | - assistant stream: `${turnId}:assistant` |
| 157 | 3. `call_id` sanitization: replace whitespace (including newline) with `_`. | 177 | - tools: `${turnId}:${toolCallId}` |
| 158 | | 178 | |
| 159 | --- | 179 | --- |
| 160 | | 180 | |
| 161 | ## 5) Approval and Checkpoint Behavior (explicit limitations) | 181 | ## 5) Approval, Resume, and Rollback Behavior |
| 162 | | 182 | |
| 163 | ### 5.1 Approvals | 183 | ### 5.1 Approvals |
| 164 | | 184 | |
| 165 | 1. Cursor CLI approval is internal; adapter cannot accept/reject mid-turn via `respondToRequest`. | 185 | 1. Cursor ACP permission requests are externally controllable; implement full `respondToRequest` path in v1. |
| 166 | 2. Plan for v1: | 186 | 2. Decision mapping: |
| 167 | - `respondToRequest` returns `ProviderAdapterRequestError` (“Cursor CLI does not expose external approval response API”). | 187 | - allow once -> `allow-once` |
| 168 | - approval-required runtime uses no `--force`, so dangerous commands become `tool_call.result.rejected`. | 188 | - allow always -> `allow-always` |
| 169 | 3. Future: | 189 | - reject -> `reject-once` |
| 170 | - if Cursor exposes approval RPC/hooks, implement `approval.requested` + `approval.resolved` fully. | 190 | |
| | 191 | ### 5.2 Resume |
| 171 | | 192 | |
| 172 | ### 5.2 Rollback / thread read | 193 | 1. `session/load` is available and should be first-class for adapter restart/reconnect. |
| | 194 | 2. Must send required params: `sessionId`, `cwd`, `mcpServers`. |
| 173 | | 195 | |
| 174 | 1. Cursor CLI has no observed equivalent to `thread/read` or `thread/rollback`. | 196 | ### 5.3 Rollback / thread read |
| | 197 | |
| | 198 | 1. ACP currently has no observed rollback API. |
| 175 | 2. Plan for v1: | 199 | 2. Plan for v1: |
| 176 | - `readThread` returns adapter-maintained in-memory turn snapshot. | 200 | - `readThread`: adapter-maintained snapshot projection |
| 177 | - `rollbackThread` returns `ProviderAdapterRequestError` unsupported. | 201 | - `rollbackThread`: explicit unsupported error |
| 178 | 3. Product guard: | 202 | 3. Product guard: |
| 179 | - disable checkpoint revert for Cursor threads until rollback is implemented. | 203 | - disable checkpoint revert for Cursor threads in UI until rollback exists. |
| 180 | | 204 | |
| 181 | --- | 205 | --- |
| 182 | | 206 | |
| 183 | ## 6) Required Contract and Runtime Changes | 207 | ## 6) Required Contract and Runtime Changes |
| 184 | | 208 | |
| 185 | ## 6.1 Contracts | 209 | ### 6.1 Contracts |
| 186 | | 210 | |
| 187 | 1. `packages/contracts/src/orchestration.ts` | 211 | 1. Add `cursor` to `ProviderKind`. |
| 188 | - add `cursor` to `ProviderKind`. | 212 | 2. Add Cursor provider start options (`providerOptions.cursor`), ACP-oriented: |
| 189 | 2. `packages/contracts/src/provider.ts` | 213 | - optional `binaryPath` |
| 190 | - add `CursorProviderStartOptions` under `providerOptions.cursor`: | 214 | - optional auth/mode knobs if needed later |
| 191 | - `binaryPath?` | 215 | 3. Extend model options for Cursor list and traits mapping. |
| 192 | - `apiKey?` (optional; default login auth) | 216 | 4. Add schemas for ACP-native event union in Cursor adapter service file. |
| 193 | - `trust?` (default true for headless) | 217 | |
| 194 | 3. `packages/contracts/src/model.ts` | 218 | ### 6.2 Server orchestration and registry |
| 195 | - add `cursor` provider model options baseline. | 219 | |
| 196 | - initial set should mirror observed supported IDs that are stable enough for UX. | 220 | 1. Register `CursorAdapter` in provider registry and server layer wiring. |
| 197 | 4. Update related contract tests. | 221 | 2. Update provider-kind persistence decoding for `cursor`. |
| 198 | | 222 | 3. Fix `ProviderCommandReactor` precedence to honor explicit provider in turn-start command. |
| 199 | ## 6.2 Server orchestration and registry | 223 | |
| 200 | | 224 | ### 6.3 Web |
| 201 | 1. Add `CursorAdapter` to provider registry and server layer wiring. | 225 | |
| 202 | 2. Update provider-kind decoding in: | 226 | 1. Cursor in provider picker and model picker (already partially done). |
| 203 | - `apps/server/src/provider/Layers/ProviderSessionDirectory.ts` | 227 | 2. Trait controls map to concrete Cursor model identifiers. |
| 204 | - persistence schemas that currently literal-match provider kinds. | 228 | 3. Surface unsupported rollback behavior in UX. |
| 205 | 3. Fix provider selection in `ProviderCommandReactor`: | | |
| 206 | - honor `thread.turn-start-requested.payload.provider` as highest precedence. | | |
| 207 | - fallback to existing session provider, then default. | | |
| 208 | | | |
| 209 | ## 6.3 Web | | |
| 210 | | | |
| 211 | 1. Enable Cursor in provider selector (`apps/web/src/session-logic.ts`). | | |
| 212 | 2. Add Cursor label/icon treatment. | | |
| 213 | 3. Add optional Cursor settings fields if using per-user overrides (binary path/api key). | | |
| 214 | 4. Ensure thread/provider display and legacy mapping code handles `cursor`. | | |
| 215 | | 229 | |
| 216 | --- | 230 | --- |
| 217 | | 231 | |
| 218 | ## 7) Implementation Phases | 232 | ## 7) Implementation Phases |
| 219 | | 233 | |
| 220 | ### Phase A: Protocol-safe adapter skeleton | 234 | ### Phase A: ACP process and protocol skeleton |
| 221 | | 235 | |
| 222 | 1. Implement `CursorAdapter` service/layer with all runtime logic in `CursorAdapterLive` (no CLI manager), plus session map + event queue. | 236 | 1. Implement ACP process lifecycle in `CursorAdapterLive`. |
| 223 | 2. Implement robust NDJSON parser with: | 237 | 2. Implement JSON-RPC request/response multiplexer. |
| 224 | - line buffering | 238 | 3. Implement `initialize`/`authenticate`/`session/new|load` flow. |
| 225 | - tolerant non-JSON handling | 239 | 4. Wire `streamEvents` from ACP notifications. |
| 226 | - explicit process-exit mapping. | | |
| 227 | 3. Wire into registry and server layers. | | |
| 228 | | 240 | |
| 229 | ### Phase B: Event projection completeness | 241 | ### Phase B: Runtime projection and approvals |
| 230 | | 242 | |
| 231 | 1. Map assistant/tool/result events to canonical runtime events. | 243 | 1. Map `session/update` variants to canonical runtime events. |
| 232 | 2. Add synthetic ID strategy and item lifecycle consistency. | 244 | 2. Implement permission-request bridging to `respondToRequest`. |
| 233 | 3. Add runtime error and retry handling. | 245 | 3. Implement dedupe for repeated `tool_call` on same `toolCallId`. |
| 234 | | 246 | |
| 235 | ### Phase C: Provider selection and UX | 247 | ### Phase C: Turn control and interruption |
| 236 | | 248 | |
| 237 | 1. Fix `ProviderCommandReactor` provider precedence. | 249 | 1. Implement single in-flight prompt protection per session. |
| 238 | 2. Enable Cursor provider in web UI. | 250 | 2. Implement interruption fallback (process restart + reload) because `session/cancel` unavailable. |
| 239 | 3. Add Cursor model defaults/options. | 251 | 3. Ensure clean state recovery on ACP process crash. |
| 240 | | 252 | |
| 241 | ### Phase D: Safeguards and unsupported surfaces | 253 | ### Phase D: Orchestration + UX polish |
| 242 | | 254 | |
| 243 | 1. Explicit unsupported errors for `respondToRequest` and `rollbackThread` (v1). | 255 | 1. Provider routing precedence fix. |
| 244 | 2. Guard checkpoint-revert path for Cursor threads. | 256 | 2. Cursor-specific UX notes for unsupported rollback. |
| 245 | 3. Document limitations in UI/help text. | 257 | 3. End-to-end smoke and event log validation. |
| 246 | | 258 | |
| 247 | --- | 259 | --- |
| 248 | | 260 | |
| ... | @@ -252,59 +264,64 @@ Follow project rule: backend external-service integrations tested via layered fa | ... | @@ -252,59 +264,64 @@ Follow project rule: backend external-service integrations tested via layered fa |
| 252 | | 264 | |
| 253 | ### 8.1 Unit tests (`CursorAdapter`) | 265 | ### 8.1 Unit tests (`CursorAdapter`) |
| 254 | | 266 | |
| 255 | 1. stream JSON parse and mapping: | 267 | 1. JSON-RPC envelope parsing: |
| 256 | - assistant partial/final | 268 | - response matching by id |
| 257 | - each tool type | 269 | - server request handling (`session/request_permission`) |
| 258 | - shell success/failure/rejected | 270 | - notification decode (`session/update`) |
| 259 | - non-JSON line failure | 271 | 2. Event projection: |
| 260 | 2. process lifecycle: | 272 | - `agent_message_chunk` / `agent_thought_chunk` |
| 261 | - start/send/interrupt/stop | 273 | - `tool_call` + `tool_call_update` dedupe/lifecycle |
| 262 | - stale session errors | 274 | - permission request -> approval events |
| 263 | 3. ID sanitization: | 275 | 3. Error mapping: |
| 264 | - `call_id` with newline. | 276 | - unknown session |
| | 277 | - method-not-found (`session/cancel`) |
| | 278 | - invalid params |
| 265 | | 279 | |
| 266 | ### 8.2 Provider service/routing tests | 280 | ### 8.2 Provider service/routing tests |
| 267 | | 281 | |
| 268 | 1. Registry resolves `cursor`. | 282 | 1. Registry resolves `cursor`. |
| 269 | 2. Session directory persistence reads/writes `cursor` provider. | 283 | 2. Session directory persistence reads/writes `cursor`. |
| 270 | 3. ProviderService fan-out/order with Cursor events. | 284 | 3. ProviderService fan-out ordering with Cursor ACP events. |
| 271 | | 285 | |
| 272 | ### 8.3 Orchestration tests | 286 | ### 8.3 Orchestration tests |
| 273 | | 287 | |
| 274 | 1. `thread.turn.start` with `provider: cursor` routes to Cursor adapter. | 288 | 1. `thread.turn.start` with `provider: cursor` routes to Cursor adapter. |
| 275 | 2. checkpoint revert on Cursor thread returns controlled failure activity. | 289 | 2. approval response command maps to ACP permission response. |
| 276 | 3. approval response command on Cursor session surfaces “unsupported approval API”. | 290 | 3. checkpoint revert on Cursor thread returns controlled unsupported failure. |
| 277 | | 291 | |
| 278 | ### 8.4 Smoke/integration harness | 292 | ### 8.4 Optional live smoke |
| 279 | | 293 | |
| 280 | 1. Optional local smoke behind env flag (`CURSOR_SMOKE=1`) to run real `agent`: | 294 | 1. Env-gated ACP smoke: |
| 281 | - create session | 295 | - start session |
| 282 | - send simple turn | 296 | - run prompt |
| 283 | - verify message delta/completion + turn completion. | 297 | - observe deltas + completion |
| | 298 | - exercise permission request path with one tool call |
| 284 | | 299 | |
| 285 | --- | 300 | --- |
| 286 | | 301 | |
| 287 | ## 9) Operational Notes | 302 | ## 9) Operational Notes |
| 288 | | 303 | |
| 289 | 1. Do not run concurrent `agent` processes per workspace by default; serialize per adapter session to avoid config races. | 304 | 1. Keep one in-flight turn per ACP session. |
| 290 | 2. Keep native event NDJSON logs for Cursor similar to Codex adapter logs. | 305 | 2. Keep per-session ACP process logs/NDJSON artifacts for debugging. |
| 291 | 3. Treat startup plain-text failures as process-level errors and surface full message. | 306 | 3. Treat `session/cancel` as unsupported until Cursor ships it; avoid relying on it. |
| | 307 | 4. Preserve resume metadata (`acpSessionId`) for crash recovery. |
| 292 | | 308 | |
| 293 | --- | 309 | --- |
| 294 | | 310 | |
| 295 | ## 10) Open Questions | 311 | ## 10) Open Questions |
| 296 | | 312 | |
| 297 | 1. Should Cursor provider expose full model list dynamically (`agent --list-models`) or use curated static list in contracts? | 313 | 1. Should we call `authenticate` always, or only after auth-required errors? |
| 298 | 2. Should T3 keep strict approval UX parity with Codex, or accept Cursor’s current non-interactive approval limitation in v1? | 314 | 2. Should model selection be passed at `session/new` only, or can/should we support model switching mid-session if ACP adds API? |
| 299 | 3. Is checkpoint revert required for initial Cursor release, or can it ship as explicitly unsupported? | 315 | 3. For interruption UX, do we expose “hard interrupt” semantics (process restart) explicitly? |
| 300 | | 316 | |
| 301 | --- | 317 | --- |
| 302 | | 318 | |
| 303 | ## 11) Delivery Checklist | 319 | ## 11) Delivery Checklist |
| 304 | | 320 | |
| 305 | 1. Contracts updated (`ProviderKind`, provider start options, models). | 321 | 1. Plan/documentation switched from headless `agent -p` to ACP `agent acp`. |
| 306 | 2. Cursor adapter + layer implemented and registered. | 322 | 2. Contracts updated (`ProviderKind`, Cursor options, model/trait mapping). |
| 307 | 3. Provider selection precedence fixed in `ProviderCommandReactor`. | 323 | 3. Cursor ACP adapter layer implemented and registered. |
| 308 | 4. Cursor enabled in web provider picker. | 324 | 4. Provider precedence fixed in orchestration router. |
| 309 | 5. Tests added for adapter mapping, routing, and orchestration behavior. | 325 | 5. Approval response path wired through ACP permission requests. |
| 310 | 6. Lint + test suite green. | 326 | 6. Tests added for protocol decode, projection, approval flow, and routing. |
| | 327 | 7. Lint + tests green. |