|
| 1 | +# Plan: Cursor ACP (`agent acp`) Provider Integration |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 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 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1) Exploration Findings (from live ACP probes) |
| 10 | + |
| 11 | +### 1.1 Core invocation and transport |
| 12 | + |
| 13 | +1. Binary is `agent` on PATH (`2026.02.27-e7d2ef6` observed). |
| 14 | +2. ACP server command is `agent acp`. |
| 15 | +3. Transport is newline-delimited JSON-RPC 2.0 over stdio. |
| 16 | +4. Messages: |
| 17 | + - client -> server: requests and responses to server-initiated requests |
| 18 | + - server -> client: responses, notifications (`session/update`), and server requests (`session/request_permission`) |
| 19 | + |
| 20 | +### 1.2 Handshake and session calls observed |
| 21 | + |
| 22 | +1. `initialize` returns: |
| 23 | + - `protocolVersion` |
| 24 | + - `agentCapabilities` (`loadSession`, `mcpCapabilities`, `promptCapabilities`) |
| 25 | + - `authMethods` (includes `cursor_login`) |
| 26 | +2. `authenticate { methodId: "cursor_login" }` returns `{}` when logged in. |
| 27 | +3. `session/new` returns: |
| 28 | + - `sessionId` |
| 29 | + - `modes` (`agent`, `plan`, `ask`) |
| 30 | +4. `session/load` works and requires `sessionId`, `cwd`, `mcpServers`. |
| 31 | +5. `session/prompt` returns terminal response `{ stopReason: "end_turn" | "cancelled" }`. |
| 32 | + |
| 33 | +Important sequence note: |
| 34 | +1. ACP currently allows `session/new` even without explicit `initialize`/`authenticate` when local auth already exists. |
| 35 | +2. For adapter consistency and forward compatibility, we should still send `initialize` and `authenticate` during startup. |
| 36 | + |
| 37 | +### 1.3 `session/update` event families observed |
| 38 | + |
| 39 | +Observed `params.update.sessionUpdate` values: |
| 40 | + |
| 41 | +1. `available_commands_update` |
| 42 | +2. `agent_thought_chunk` |
| 43 | +3. `agent_message_chunk` |
| 44 | +4. `tool_call` |
| 45 | +5. `tool_call_update` |
| 46 | + |
| 47 | +Observed payload behavior: |
| 48 | + |
| 49 | +1. `agent_*_chunk` provides `content: { type: "text", text: string }`. |
| 50 | +2. `tool_call` may be emitted multiple times for same `toolCallId`: |
| 51 | + - initial generic form (`title: "Terminal"`, `rawInput: {}`) |
| 52 | + - enriched form (`title: "\`pwd\`"`, `rawInput: { command: "pwd" }`) |
| 53 | +3. `tool_call_update` statuses observed: |
| 54 | + - `in_progress` |
| 55 | + - `completed` |
| 56 | +4. `tool_call_update` on completion may include `rawOutput`: |
| 57 | + - terminal: `{ exitCode, stdout, stderr }` |
| 58 | + - search/find: `{ totalFiles, truncated }` |
| 59 | + |
| 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` |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +## 2) Integration Constraints for T3 |
| 91 | + |
| 92 | +1. T3 adapter contract still requires: |
| 93 | + - `startSession`, `sendTurn`, `interruptTurn`, `respondToRequest`, `readThread`, `rollbackThread`, `stopSession`, `listSessions`, `hasSession`, `stopAll`, `streamEvents`. |
| 94 | +2. Orchestration consumes canonical `ProviderRuntimeEvent` only. |
| 95 | +3. `ProviderCommandReactor` provider precedence fix remains required (respect explicit provider on turn start). |
| 96 | +4. ACP now supports external permission decisions, so Cursor can participate in T3 approval UX via adapter-managed request/response plumbing. |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## 3) Proposed Architecture |
| 101 | + |
| 102 | +### 3.1 New server components |
| 103 | + |
| 104 | +1. `apps/server/src/provider/Services/CursorAdapter.ts` (service contract/tag + ACP event schemas). |
| 105 | +2. `apps/server/src/provider/Layers/CursorAdapter.ts` (single implementation unit; owns ACP process lifecycle, JSON-RPC routing, runtime projection). |
| 106 | +3. No manager indirection; keep logic in layer implementation. |
| 107 | + |
| 108 | +### 3.2 Session model |
| 109 | + |
| 110 | +1. One long-lived ACP child process per T3 Cursor provider session. |
| 111 | +2. Track: |
| 112 | + - `providerSessionId` (T3 synthetic ID) |
| 113 | + - `acpSessionId` (from `session/new` or restored via `session/load`) |
| 114 | + - `cwd`, `model`, in-flight turn state |
| 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. |
| 118 | + |
| 119 | +### 3.3 Command strategy |
| 120 | + |
| 121 | +1. `startSession`: |
| 122 | + - spawn `agent acp` |
| 123 | + - `initialize` |
| 124 | + - `authenticate(cursor_login)` (best-effort, typed failure handling) |
| 125 | + - `session/new` or `session/load` |
| 126 | +2. `sendTurn`: |
| 127 | + - send `session/prompt { sessionId, prompt: [...] }` |
| 128 | + - consume streaming `session/update` notifications until terminal prompt response |
| 129 | +3. `interruptTurn`: |
| 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` |
| 136 | + |
| 137 | +### 3.4 Effect-first implementation style (required) |
| 138 | + |
| 139 | +1. Keep logic inside `CursorAdapterLive`. |
| 140 | +2. Use Effect primitives: |
| 141 | + - `Queue` + `Stream.fromQueue` for event fan-out |
| 142 | + - `Ref` / `Ref.Synchronized` for session/process/request state |
| 143 | + - scoped fibers for stdout/stderr read loops |
| 144 | +3. Typed JSON decode at boundary: |
| 145 | + - request/response envelopes |
| 146 | + - `session/update` union schema |
| 147 | + - permission-request schema |
| 148 | +4. Keep adapter errors in typed error algebra with explicit mapping at process/protocol boundaries. |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## 4) Canonical Event Mapping Plan (ACP -> ProviderRuntimeEvent) |
| 153 | + |
| 154 | +1. `session/update: agent_message_chunk` |
| 155 | + - emit `message.delta` for assistant stream |
| 156 | +2. prompt terminal response (`session/prompt` result `stopReason: end_turn`) |
| 157 | + - emit `message.completed` + `turn.completed` |
| 158 | +3. `session/update: agent_thought_chunk` |
| 159 | + - initial mapping: emit thinking activity (or ignore if we keep current canonical surface minimal) |
| 160 | +4. `session/update: tool_call` |
| 161 | + - first-seen `toolCallId` emits `tool.started` |
| 162 | + - subsequent `tool_call` for same ID treated as metadata update (no duplicate started event) |
| 163 | +5. `session/update: tool_call_update` |
| 164 | + - `in_progress`: optional progress activity |
| 165 | + - `completed`: emit `tool.completed` with summarized `rawOutput` when present |
| 166 | +6. `session/request_permission` |
| 167 | + - emit `approval.requested` with mapped options |
| 168 | + - when client decision sent, emit `approval.resolved` |
| 169 | +7. protocol/process error |
| 170 | + - emit `runtime.error` |
| 171 | + - fail active turn/session as appropriate |
| 172 | + |
| 173 | +Synthetic IDs: |
| 174 | +1. `turnId`: T3-generated UUID per `sendTurn`. |
| 175 | +2. `itemId`: |
| 176 | + - assistant stream: `${turnId}:assistant` |
| 177 | + - tools: `${turnId}:${toolCallId}` |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## 5) Approval, Resume, and Rollback Behavior |
| 182 | + |
| 183 | +### 5.1 Approvals |
| 184 | + |
| 185 | +1. Cursor ACP permission requests are externally controllable; implement full `respondToRequest` path in v1. |
| 186 | +2. Decision mapping: |
| 187 | + - allow once -> `allow-once` |
| 188 | + - allow always -> `allow-always` |
| 189 | + - reject -> `reject-once` |
| 190 | + |
| 191 | +### 5.2 Resume |
| 192 | + |
| 193 | +1. `session/load` is available and should be first-class for adapter restart/reconnect. |
| 194 | +2. Must send required params: `sessionId`, `cwd`, `mcpServers`. |
| 195 | + |
| 196 | +### 5.3 Rollback / thread read |
| 197 | + |
| 198 | +1. ACP currently has no observed rollback API. |
| 199 | +2. Plan for v1: |
| 200 | + - `readThread`: adapter-maintained snapshot projection |
| 201 | + - `rollbackThread`: explicit unsupported error |
| 202 | +3. Product guard: |
| 203 | + - disable checkpoint revert for Cursor threads in UI until rollback exists. |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +## 6) Required Contract and Runtime Changes |
| 208 | + |
| 209 | +### 6.1 Contracts |
| 210 | + |
| 211 | +1. Add `cursor` to `ProviderKind`. |
| 212 | +2. Add Cursor provider start options (`providerOptions.cursor`), ACP-oriented: |
| 213 | + - optional `binaryPath` |
| 214 | + - optional auth/mode knobs if needed later |
| 215 | +3. Extend model options for Cursor list and traits mapping. |
| 216 | +4. Add schemas for ACP-native event union in Cursor adapter service file. |
| 217 | + |
| 218 | +### 6.2 Server orchestration and registry |
| 219 | + |
| 220 | +1. Register `CursorAdapter` in provider registry and server layer wiring. |
| 221 | +2. Update provider-kind persistence decoding for `cursor`. |
| 222 | +3. Fix `ProviderCommandReactor` precedence to honor explicit provider in turn-start command. |
| 223 | + |
| 224 | +### 6.3 Web |
| 225 | + |
| 226 | +1. Cursor in provider picker and model picker (already partially done). |
| 227 | +2. Trait controls map to concrete Cursor model identifiers. |
| 228 | +3. Surface unsupported rollback behavior in UX. |
| 229 | + |
| 230 | +--- |
| 231 | + |
| 232 | +## 7) Implementation Phases |
| 233 | + |
| 234 | +### Phase A: ACP process and protocol skeleton |
| 235 | + |
| 236 | +1. Implement ACP process lifecycle in `CursorAdapterLive`. |
| 237 | +2. Implement JSON-RPC request/response multiplexer. |
| 238 | +3. Implement `initialize`/`authenticate`/`session/new|load` flow. |
| 239 | +4. Wire `streamEvents` from ACP notifications. |
| 240 | + |
| 241 | +### Phase B: Runtime projection and approvals |
| 242 | + |
| 243 | +1. Map `session/update` variants to canonical runtime events. |
| 244 | +2. Implement permission-request bridging to `respondToRequest`. |
| 245 | +3. Implement dedupe for repeated `tool_call` on same `toolCallId`. |
| 246 | + |
| 247 | +### Phase C: Turn control and interruption |
| 248 | + |
| 249 | +1. Implement single in-flight prompt protection per session. |
| 250 | +2. Implement interruption fallback (process restart + reload) because `session/cancel` unavailable. |
| 251 | +3. Ensure clean state recovery on ACP process crash. |
| 252 | + |
| 253 | +### Phase D: Orchestration + UX polish |
| 254 | + |
| 255 | +1. Provider routing precedence fix. |
| 256 | +2. Cursor-specific UX notes for unsupported rollback. |
| 257 | +3. End-to-end smoke and event log validation. |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +## 8) Test Plan |
| 262 | + |
| 263 | +Follow project rule: backend external-service integrations tested via layered fakes, not by mocking core business logic. |
| 264 | + |
| 265 | +### 8.1 Unit tests (`CursorAdapter`) |
| 266 | + |
| 267 | +1. JSON-RPC envelope parsing: |
| 268 | + - response matching by id |
| 269 | + - server request handling (`session/request_permission`) |
| 270 | + - notification decode (`session/update`) |
| 271 | +2. Event projection: |
| 272 | + - `agent_message_chunk` / `agent_thought_chunk` |
| 273 | + - `tool_call` + `tool_call_update` dedupe/lifecycle |
| 274 | + - permission request -> approval events |
| 275 | +3. Error mapping: |
| 276 | + - unknown session |
| 277 | + - method-not-found (`session/cancel`) |
| 278 | + - invalid params |
| 279 | + |
| 280 | +### 8.2 Provider service/routing tests |
| 281 | + |
| 282 | +1. Registry resolves `cursor`. |
| 283 | +2. Session directory persistence reads/writes `cursor`. |
| 284 | +3. ProviderService fan-out ordering with Cursor ACP events. |
| 285 | + |
| 286 | +### 8.3 Orchestration tests |
| 287 | + |
| 288 | +1. `thread.turn.start` with `provider: cursor` routes to Cursor adapter. |
| 289 | +2. approval response command maps to ACP permission response. |
| 290 | +3. checkpoint revert on Cursor thread returns controlled unsupported failure. |
| 291 | + |
| 292 | +### 8.4 Optional live smoke |
| 293 | + |
| 294 | +1. Env-gated ACP smoke: |
| 295 | + - start session |
| 296 | + - run prompt |
| 297 | + - observe deltas + completion |
| 298 | + - exercise permission request path with one tool call |
| 299 | + |
| 300 | +--- |
| 301 | + |
| 302 | +## 9) Operational Notes |
| 303 | + |
| 304 | +1. Keep one in-flight turn per ACP session. |
| 305 | +2. Keep per-session ACP process logs/NDJSON artifacts for debugging. |
| 306 | +3. Treat `session/cancel` as unsupported until Cursor ships it; avoid relying on it. |
| 307 | +4. Preserve resume metadata (`acpSessionId`) for crash recovery. |
| 308 | + |
| 309 | +--- |
| 310 | + |
| 311 | +## 10) Open Questions |
| 312 | + |
| 313 | +1. Should we call `authenticate` always, or only after auth-required errors? |
| 314 | +2. Should model selection be passed at `session/new` only, or can/should we support model switching mid-session if ACP adds API? |
| 315 | +3. For interruption UX, do we expose “hard interrupt” semantics (process restart) explicitly? |
| 316 | + |
| 317 | +--- |
| 318 | + |
| 319 | +## 11) Delivery Checklist |
| 320 | + |
| 321 | +1. Plan/documentation switched from headless `agent -p` to ACP `agent acp`. |
| 322 | +2. Contracts updated (`ProviderKind`, Cursor options, model/trait mapping). |
| 323 | +3. Cursor ACP adapter layer implemented and registered. |
| 324 | +4. Provider precedence fixed in orchestration router. |
| 325 | +5. Approval response path wired through ACP permission requests. |
| 326 | +6. Tests added for protocol decode, projection, approval flow, and routing. |
| 327 | +7. Lint + tests green. |
0 commit comments