| author | |
| committer | |
| log | 04a931fc7992152b388faef5cda28e64a0887303 |
| tree | 02088fdb26458b26615e651fd320c825f460c81f |
| parent | 75354bbd962d9fc38a1ba40ee78520822b349016 |
- define `CursorAdapter` service contract and Cursor stream-json schema types
- add `CursorCliStreamEvent` decoding tests for system/thinking/tool/result/retry events
- add implementation plan for Cursor provider integration
Co-authored-by: codex <codex@users.noreply.github.com>3 files changed, 656 insertions(+), 0 deletions(-)
.plans/18-cursor-agent-provider.md created+310| ... | ... | @@ -0,0 +1,310 @@ |
| 1 | # Plan: Cursor CLI (`agent`) Provider Integration | |
| 2 | ||
| 3 | ## Goal | |
| 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. | |
| 6 | ||
| 7 | --- | |
| 8 | ||
| 9 | ## 1) Exploration Findings (from live CLI runs) | |
| 10 | ||
| 11 | ### 1.1 Core invocation shape | |
| 12 | ||
| 13 | 1. Binary is `agent` on PATH (`2026.02.27-e7d2ef6` observed). | |
| 14 | 2. Non-interactive streaming mode: | |
| 15 | - `agent -p --trust --output-format stream-json --stream-partial-output ...` | |
| 16 | 3. Session continuity: | |
| 17 | - `agent create-chat` returns a chat UUID. | |
| 18 | - `--resume <chatId>` and `--continue` preserve history. | |
| 19 | - Stream `session_id` equals chat UUID. | |
| 20 | ||
| 21 | ### 1.2 Stream JSON event families observed | |
| 22 | ||
| 23 | 1. `system/init` | |
| 24 | 2. `user` | |
| 25 | 3. `thinking/delta`, `thinking/completed` | |
| 26 | 4. `assistant` (partial and final) | |
| 27 | 5. `tool_call/started`, `tool_call/completed` | |
| 28 | 6. `result/success` (`is_error: false` in all successful process exits) | |
| 29 | 7. Previously observed under load/rate-limit: | |
| 30 | - `connection/reconnecting`, `connection/reconnected` | |
| 31 | - `retry/starting`, `retry/resuming` | |
| 32 | - followed by non-JSON terminal line `v: [resource_exhausted] Error` and process exit code `1` | |
| 33 | ||
| 34 | ### 1.3 Tool call variants observed | |
| 35 | ||
| 36 | 1. `shellToolCall` | |
| 37 | 2. `readToolCall` | |
| 38 | 3. `editToolCall` | |
| 39 | 4. `grepToolCall` | |
| 40 | 5. `globToolCall` | |
| 41 | ||
| 42 | Tool completion outcomes observed: | |
| 43 | ||
| 44 | 1. `result.success` | |
| 45 | 2. `result.failure` (non-zero exit, stderr) | |
| 46 | 3. `result.rejected` (permission denied by Cursor approval layer) | |
| 47 | ||
| 48 | ### 1.4 Critical protocol quirks | |
| 49 | ||
| 50 | 1. `tool_call.call_id` contains embedded newline characters (must sanitize for IDs). | |
| 51 | 2. Startup failures are plain text, not JSON: | |
| 52 | - invalid model -> plain error + exit `1` | |
| 53 | - invalid API key -> plain warning + exit `1` | |
| 54 | 3. `result.subtype` can be `success` even when tool calls failed/rejected inside the turn. | |
| 55 | 4. Running multiple `agent` commands concurrently can race on `~/.cursor/cli-config.json` writes (`ENOENT rename ...cli-config.json.tmp` observed). | |
| 56 | ||
| 57 | Fixtures recorded in repo: | |
| 58 | ||
| 59 | 1. `.tmp/cursor-observations/*.ndjson` | |
| 60 | 2. `.tmp/cursor-observations/startup-invalid-*.txt` | |
| 61 | ||
| 62 | --- | |
| 63 | ||
| 64 | ## 2) Integration Constraints for T3 | |
| 65 | ||
| 66 | 1. T3 requires adapter contract support for: | |
| 67 | - `startSession`, `sendTurn`, `interruptTurn`, `respondToRequest`, `readThread`, `rollbackThread`, `stopSession`, `listSessions`, `hasSession`, `stopAll`, `streamEvents`. | |
| 68 | 2. Orchestration depends on canonical runtime events (`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). | |
| 70 | 4. Cursor CLI currently has no external approval callback API; approvals are internal to CLI behavior. | |
| 71 | ||
| 72 | --- | |
| 73 | ||
| 74 | ## 3) Proposed Architecture | |
| 75 | ||
| 76 | ## 3.1 New server components | |
| 77 | ||
| 78 | 1. `apps/server/src/provider/Services/CursorAdapter.ts` (service contract/tag only for DI parity) | |
| 79 | 2. `apps/server/src/provider/Layers/CursorAdapter.ts` (single implementation unit; owns Cursor process lifecycle + stream parsing + runtime projection) | |
| 80 | 3. No separate `cursorCliManager.ts` abstraction in v1. | |
| 81 | ||
| 82 | ### 3.2 Session model | |
| 83 | ||
| 84 | 1. T3 `ProviderSessionId` = synthetic UUID managed by adapter (stable for T3 APIs). | |
| 85 | 2. Cursor chat UUID stored as: | |
| 86 | - `ProviderSession.resumeCursor = { chatId: string }` | |
| 87 | - `ProviderSession.threadId = ProviderThreadId(chatId)` for adapter contract compatibility. | |
| 88 | 3. Adapter keeps runtime map: | |
| 89 | - `sessionId -> { chatId, cwd, model, activeChildProcess?, activeTurnId?, turnLog }` | |
| 90 | ||
| 91 | ### 3.3 Command strategy | |
| 92 | ||
| 93 | 1. `startSession`: | |
| 94 | - call `agent create-chat` unless `resumeCursor.chatId` exists. | |
| 95 | - validate provider is `cursor` when provided. | |
| 96 | 2. `sendTurn`: | |
| 97 | - spawn `agent -p --trust --output-format stream-json --stream-partial-output --resume <chatId>`. | |
| 98 | - add `--model` when provided. | |
| 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`: | |
| 104 | - terminate active child process for session (`SIGINT` then hard kill fallback). | |
| 105 | ||
| 106 | ### 3.4 Effect-first implementation style (required) | |
| 107 | ||
| 108 | 1. Keep adapter logic inside `CursorAdapterLive` layer constructor (no manager indirection). | |
| 109 | 2. Use Effect process primitives first: | |
| 110 | - `ChildProcessSpawner` + `ChildProcess.make` from `effect/unstable/process` for `agent` execution. | |
| 111 | 3. Use Effect concurrency/state primitives: | |
| 112 | - `Queue` for adapter event queue, `Stream.fromQueue` for `streamEvents` | |
| 113 | - `Ref` / `Ref.Synchronized` for session maps and active-turn process handles | |
| 114 | - `Effect.scoped` + `Effect.forkScoped` for worker fibers and cleanup | |
| 115 | 4. Use Effect stream parsing path for stdout/stderr: | |
| 116 | - decode bytes -> line buffer -> JSON parse -> typed projection | |
| 117 | - keep non-JSON lines on a fallback branch that emits `runtime.error` | |
| 118 | 5. Keep errors in typed adapter error algebra (`ProviderAdapter*Error`) via `Effect.mapError` boundaries, not ad-hoc exceptions. | |
| 119 | ||
| 120 | --- | |
| 121 | ||
| 122 | ## 4) Canonical Event Mapping Plan | |
| 123 | ||
| 124 | For each parsed NDJSON line, emit zero or more `ProviderRuntimeEvent`s: | |
| 125 | ||
| 126 | 1. `system/init` | |
| 127 | - emit `session.started` (once per adapter session lifecycle) | |
| 128 | - emit `thread.started` if thread not yet emitted | |
| 129 | 2. `assistant` partial chunks | |
| 130 | - emit `message.delta` with stable per-turn synthetic `itemId` | |
| 131 | 3. final `assistant` message | |
| 132 | - emit `message.completed` | |
| 133 | 4. `tool_call/started` | |
| 134 | - emit `tool.started` | |
| 135 | - map kind: | |
| 136 | - shell -> `command` | |
| 137 | - edit -> `file-change` | |
| 138 | - read/grep/glob -> `other` | |
| 139 | 5. `tool_call/completed` | |
| 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` | |
| 148 | - emit `turn.completed` with `status: failed` | |
| 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 | ||
| 153 | Synthetic IDs: | |
| 154 | ||
| 155 | 1. `turnId`: adapter-generated UUID per `sendTurn`. | |
| 156 | 2. `itemId`: `${turnId}:assistant` for assistant stream; `${turnId}:${sanitizedCallId}` for tool calls. | |
| 157 | 3. `call_id` sanitization: replace whitespace (including newline) with `_`. | |
| 158 | ||
| 159 | --- | |
| 160 | ||
| 161 | ## 5) Approval and Checkpoint Behavior (explicit limitations) | |
| 162 | ||
| 163 | ### 5.1 Approvals | |
| 164 | ||
| 165 | 1. Cursor CLI approval is internal; adapter cannot accept/reject mid-turn via `respondToRequest`. | |
| 166 | 2. Plan for v1: | |
| 167 | - `respondToRequest` returns `ProviderAdapterRequestError` (“Cursor CLI does not expose external approval response API”). | |
| 168 | - approval-required runtime uses no `--force`, so dangerous commands become `tool_call.result.rejected`. | |
| 169 | 3. Future: | |
| 170 | - if Cursor exposes approval RPC/hooks, implement `approval.requested` + `approval.resolved` fully. | |
| 171 | ||
| 172 | ### 5.2 Rollback / thread read | |
| 173 | ||
| 174 | 1. Cursor CLI has no observed equivalent to `thread/read` or `thread/rollback`. | |
| 175 | 2. Plan for v1: | |
| 176 | - `readThread` returns adapter-maintained in-memory turn snapshot. | |
| 177 | - `rollbackThread` returns `ProviderAdapterRequestError` unsupported. | |
| 178 | 3. Product guard: | |
| 179 | - disable checkpoint revert for Cursor threads until rollback is implemented. | |
| 180 | ||
| 181 | --- | |
| 182 | ||
| 183 | ## 6) Required Contract and Runtime Changes | |
| 184 | ||
| 185 | ## 6.1 Contracts | |
| 186 | ||
| 187 | 1. `packages/contracts/src/orchestration.ts` | |
| 188 | - add `cursor` to `ProviderKind`. | |
| 189 | 2. `packages/contracts/src/provider.ts` | |
| 190 | - add `CursorProviderStartOptions` under `providerOptions.cursor`: | |
| 191 | - `binaryPath?` | |
| 192 | - `apiKey?` (optional; default login auth) | |
| 193 | - `trust?` (default true for headless) | |
| 194 | 3. `packages/contracts/src/model.ts` | |
| 195 | - add `cursor` provider model options baseline. | |
| 196 | - initial set should mirror observed supported IDs that are stable enough for UX. | |
| 197 | 4. Update related contract tests. | |
| 198 | ||
| 199 | ## 6.2 Server orchestration and registry | |
| 200 | ||
| 201 | 1. Add `CursorAdapter` to provider registry and server layer wiring. | |
| 202 | 2. Update provider-kind decoding in: | |
| 203 | - `apps/server/src/provider/Layers/ProviderSessionDirectory.ts` | |
| 204 | - persistence schemas that currently literal-match provider kinds. | |
| 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 | ||
| 216 | --- | |
| 217 | ||
| 218 | ## 7) Implementation Phases | |
| 219 | ||
| 220 | ### Phase A: Protocol-safe adapter skeleton | |
| 221 | ||
| 222 | 1. Implement `CursorAdapter` service/layer with all runtime logic in `CursorAdapterLive` (no CLI manager), plus session map + event queue. | |
| 223 | 2. Implement robust NDJSON parser with: | |
| 224 | - line buffering | |
| 225 | - tolerant non-JSON handling | |
| 226 | - explicit process-exit mapping. | |
| 227 | 3. Wire into registry and server layers. | |
| 228 | ||
| 229 | ### Phase B: Event projection completeness | |
| 230 | ||
| 231 | 1. Map assistant/tool/result events to canonical runtime events. | |
| 232 | 2. Add synthetic ID strategy and item lifecycle consistency. | |
| 233 | 3. Add runtime error and retry handling. | |
| 234 | ||
| 235 | ### Phase C: Provider selection and UX | |
| 236 | ||
| 237 | 1. Fix `ProviderCommandReactor` provider precedence. | |
| 238 | 2. Enable Cursor provider in web UI. | |
| 239 | 3. Add Cursor model defaults/options. | |
| 240 | ||
| 241 | ### Phase D: Safeguards and unsupported surfaces | |
| 242 | ||
| 243 | 1. Explicit unsupported errors for `respondToRequest` and `rollbackThread` (v1). | |
| 244 | 2. Guard checkpoint-revert path for Cursor threads. | |
| 245 | 3. Document limitations in UI/help text. | |
| 246 | ||
| 247 | --- | |
| 248 | ||
| 249 | ## 8) Test Plan | |
| 250 | ||
| 251 | Follow project rule: backend external-service integrations tested via layered fakes, not by mocking core business logic. | |
| 252 | ||
| 253 | ### 8.1 Unit tests (`CursorAdapter`) | |
| 254 | ||
| 255 | 1. stream JSON parse and mapping: | |
| 256 | - assistant partial/final | |
| 257 | - each tool type | |
| 258 | - shell success/failure/rejected | |
| 259 | - non-JSON line failure | |
| 260 | 2. process lifecycle: | |
| 261 | - start/send/interrupt/stop | |
| 262 | - stale session errors | |
| 263 | 3. ID sanitization: | |
| 264 | - `call_id` with newline. | |
| 265 | ||
| 266 | ### 8.2 Provider service/routing tests | |
| 267 | ||
| 268 | 1. Registry resolves `cursor`. | |
| 269 | 2. Session directory persistence reads/writes `cursor` provider. | |
| 270 | 3. ProviderService fan-out/order with Cursor events. | |
| 271 | ||
| 272 | ### 8.3 Orchestration tests | |
| 273 | ||
| 274 | 1. `thread.turn.start` with `provider: cursor` routes to Cursor adapter. | |
| 275 | 2. checkpoint revert on Cursor thread returns controlled failure activity. | |
| 276 | 3. approval response command on Cursor session surfaces “unsupported approval API”. | |
| 277 | ||
| 278 | ### 8.4 Smoke/integration harness | |
| 279 | ||
| 280 | 1. Optional local smoke behind env flag (`CURSOR_SMOKE=1`) to run real `agent`: | |
| 281 | - create session | |
| 282 | - send simple turn | |
| 283 | - verify message delta/completion + turn completion. | |
| 284 | ||
| 285 | --- | |
| 286 | ||
| 287 | ## 9) Operational Notes | |
| 288 | ||
| 289 | 1. Do not run concurrent `agent` processes per workspace by default; serialize per adapter session to avoid config races. | |
| 290 | 2. Keep native event NDJSON logs for Cursor similar to Codex adapter logs. | |
| 291 | 3. Treat startup plain-text failures as process-level errors and surface full message. | |
| 292 | ||
| 293 | --- | |
| 294 | ||
| 295 | ## 10) Open Questions | |
| 296 | ||
| 297 | 1. Should Cursor provider expose full model list dynamically (`agent --list-models`) or use curated static list in contracts? | |
| 298 | 2. Should T3 keep strict approval UX parity with Codex, or accept Cursor’s current non-interactive approval limitation in v1? | |
| 299 | 3. Is checkpoint revert required for initial Cursor release, or can it ship as explicitly unsupported? | |
| 300 | ||
| 301 | --- | |
| 302 | ||
| 303 | ## 11) Delivery Checklist | |
| 304 | ||
| 305 | 1. Contracts updated (`ProviderKind`, provider start options, models). | |
| 306 | 2. Cursor adapter + layer implemented and registered. | |
| 307 | 3. Provider selection precedence fixed in `ProviderCommandReactor`. | |
| 308 | 4. Cursor enabled in web provider picker. | |
| 309 | 5. Tests added for adapter mapping, routing, and orchestration behavior. | |
| 310 | 6. Lint + test suite green. |
apps/server/src/provider/Services/CursorAdapter.test.ts created+132| ... | ... | @@ -0,0 +1,132 @@ |
| 1 | import { Schema } from "effect"; | |
| 2 | import { describe, expect, it } from "vitest"; | |
| 3 | ||
| 4 | import { CursorCliStreamEvent } from "./CursorAdapter.ts"; | |
| 5 | ||
| 6 | describe("CursorCliStreamEvent", () => { | |
| 7 | it("decodes system/init events", () => { | |
| 8 | const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 9 | type: "system", | |
| 10 | subtype: "init", | |
| 11 | session_id: "sess-1", | |
| 12 | apiKeySource: "login", | |
| 13 | cwd: "/tmp/project", | |
| 14 | model: "Auto", | |
| 15 | permissionMode: "default", | |
| 16 | }); | |
| 17 | ||
| 18 | expect(decoded.type).toBe("system"); | |
| 19 | if (decoded.type !== "system") { | |
| 20 | return; | |
| 21 | } | |
| 22 | expect(decoded.subtype).toBe("init"); | |
| 23 | expect(decoded.session_id).toBe("sess-1"); | |
| 24 | }); | |
| 25 | ||
| 26 | it("decodes thinking delta/completed events", () => { | |
| 27 | const delta = Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 28 | type: "thinking", | |
| 29 | subtype: "delta", | |
| 30 | text: "draft", | |
| 31 | session_id: "sess-1", | |
| 32 | timestamp_ms: 123, | |
| 33 | }); | |
| 34 | ||
| 35 | expect(delta.type).toBe("thinking"); | |
| 36 | if (delta.type !== "thinking") { | |
| 37 | return; | |
| 38 | } | |
| 39 | expect(delta.subtype).toBe("delta"); | |
| 40 | ||
| 41 | const completed = Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 42 | type: "thinking", | |
| 43 | subtype: "completed", | |
| 44 | session_id: "sess-1", | |
| 45 | }); | |
| 46 | ||
| 47 | expect(completed.type).toBe("thinking"); | |
| 48 | if (completed.type !== "thinking") { | |
| 49 | return; | |
| 50 | } | |
| 51 | expect(completed.subtype).toBe("completed"); | |
| 52 | }); | |
| 53 | ||
| 54 | it("decodes tool_call completed events with rejected results", () => { | |
| 55 | const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 56 | type: "tool_call", | |
| 57 | subtype: "completed", | |
| 58 | call_id: "tool-1\nline-2", | |
| 59 | session_id: "sess-1", | |
| 60 | tool_call: { | |
| 61 | shellToolCall: { | |
| 62 | args: { | |
| 63 | command: "rm -rf /tmp/nope", | |
| 64 | }, | |
| 65 | result: { | |
| 66 | rejected: { | |
| 67 | reason: "approval denied", | |
| 68 | }, | |
| 69 | }, | |
| 70 | }, | |
| 71 | }, | |
| 72 | }); | |
| 73 | ||
| 74 | expect(decoded.type).toBe("tool_call"); | |
| 75 | if (decoded.type !== "tool_call") { | |
| 76 | return; | |
| 77 | } | |
| 78 | expect(decoded.subtype).toBe("completed"); | |
| 79 | expect(decoded.call_id).toContain("\n"); | |
| 80 | }); | |
| 81 | ||
| 82 | it("decodes result success events with usage payload", () => { | |
| 83 | const decoded = Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 84 | type: "result", | |
| 85 | subtype: "success", | |
| 86 | duration_ms: 100, | |
| 87 | duration_api_ms: 90, | |
| 88 | is_error: false, | |
| 89 | result: "OK", | |
| 90 | session_id: "sess-1", | |
| 91 | request_id: "req-1", | |
| 92 | usage: { | |
| 93 | inputTokens: 10, | |
| 94 | outputTokens: 5, | |
| 95 | cacheReadTokens: 2, | |
| 96 | cacheWriteTokens: 1, | |
| 97 | }, | |
| 98 | }); | |
| 99 | ||
| 100 | expect(decoded.type).toBe("result"); | |
| 101 | if (decoded.type !== "result") { | |
| 102 | return; | |
| 103 | } | |
| 104 | expect(decoded.subtype).toBe("success"); | |
| 105 | expect(decoded.is_error).toBe(false); | |
| 106 | }); | |
| 107 | ||
| 108 | it("decodes connection/retry lifecycle events", () => { | |
| 109 | const reconnecting = Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 110 | type: "connection", | |
| 111 | subtype: "reconnecting", | |
| 112 | session_id: "sess-1", | |
| 113 | }); | |
| 114 | expect(reconnecting.type).toBe("connection"); | |
| 115 | ||
| 116 | const retry = Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 117 | type: "retry", | |
| 118 | subtype: "resuming", | |
| 119 | session_id: "sess-1", | |
| 120 | }); | |
| 121 | expect(retry.type).toBe("retry"); | |
| 122 | }); | |
| 123 | ||
| 124 | it("rejects unsupported stream event types", () => { | |
| 125 | expect(() => | |
| 126 | Schema.decodeUnknownSync(CursorCliStreamEvent)({ | |
| 127 | type: "bogus", | |
| 128 | session_id: "sess-1", | |
| 129 | }), | |
| 130 | ).toThrow(); | |
| 131 | }); | |
| 132 | }); |
apps/server/src/provider/Services/CursorAdapter.ts created+214| ... | ... | @@ -0,0 +1,214 @@ |
| 1 | /** | |
| 2 | * CursorAdapter - Cursor CLI implementation of the generic provider adapter contract. | |
| 3 | * | |
| 4 | * This service will own Cursor CLI (`agent`) process / stream-json semantics and emit | |
| 5 | * canonical provider runtime events via the shared provider adapter contract. | |
| 6 | * | |
| 7 | * This file intentionally defines raw Cursor stream-json schemas up front so the future | |
| 8 | * layer implementation can decode/validate NDJSON lines in a single place. | |
| 9 | * | |
| 10 | * @module CursorAdapter | |
| 11 | */ | |
| 12 | import { Schema, ServiceMap } from "effect"; | |
| 13 | ||
| 14 | import type { ProviderAdapterError } from "../Errors.ts"; | |
| 15 | import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; | |
| 16 | ||
| 17 | export const CursorCliSessionId = Schema.String.check(Schema.isNonEmpty()); | |
| 18 | export type CursorCliSessionId = typeof CursorCliSessionId.Type; | |
| 19 | ||
| 20 | export const CursorCliTextContentPart = Schema.Struct({ | |
| 21 | type: Schema.Literal("text"), | |
| 22 | text: Schema.String, | |
| 23 | }); | |
| 24 | export type CursorCliTextContentPart = typeof CursorCliTextContentPart.Type; | |
| 25 | ||
| 26 | export const CursorCliContentPart = Schema.Union([ | |
| 27 | CursorCliTextContentPart, | |
| 28 | Schema.Struct({ | |
| 29 | type: Schema.String, | |
| 30 | }), | |
| 31 | ]); | |
| 32 | export type CursorCliContentPart = typeof CursorCliContentPart.Type; | |
| 33 | ||
| 34 | export const CursorCliUserMessage = Schema.Struct({ | |
| 35 | role: Schema.Literal("user"), | |
| 36 | content: Schema.Array(CursorCliContentPart), | |
| 37 | }); | |
| 38 | export type CursorCliUserMessage = typeof CursorCliUserMessage.Type; | |
| 39 | ||
| 40 | export const CursorCliAssistantMessage = Schema.Struct({ | |
| 41 | role: Schema.Literal("assistant"), | |
| 42 | content: Schema.Array(CursorCliContentPart), | |
| 43 | }); | |
| 44 | export type CursorCliAssistantMessage = typeof CursorCliAssistantMessage.Type; | |
| 45 | ||
| 46 | export const CursorCliToolCallResult = Schema.Struct({ | |
| 47 | success: Schema.optional(Schema.Unknown), | |
| 48 | failure: Schema.optional(Schema.Unknown), | |
| 49 | rejected: Schema.optional(Schema.Unknown), | |
| 50 | }); | |
| 51 | export type CursorCliToolCallResult = typeof CursorCliToolCallResult.Type; | |
| 52 | ||
| 53 | export const CursorCliToolCallEntry = Schema.Struct({ | |
| 54 | args: Schema.optional(Schema.Unknown), | |
| 55 | result: Schema.optional(CursorCliToolCallResult), | |
| 56 | }); | |
| 57 | export type CursorCliToolCallEntry = typeof CursorCliToolCallEntry.Type; | |
| 58 | ||
| 59 | export const CursorCliFunctionToolCall = Schema.Struct({ | |
| 60 | name: Schema.String, | |
| 61 | arguments: Schema.String, | |
| 62 | result: Schema.optional(CursorCliToolCallResult), | |
| 63 | }); | |
| 64 | export type CursorCliFunctionToolCall = typeof CursorCliFunctionToolCall.Type; | |
| 65 | ||
| 66 | export const CursorCliToolCallPayload = Schema.Struct({ | |
| 67 | readToolCall: Schema.optional(CursorCliToolCallEntry), | |
| 68 | writeToolCall: Schema.optional(CursorCliToolCallEntry), | |
| 69 | editToolCall: Schema.optional(CursorCliToolCallEntry), | |
| 70 | shellToolCall: Schema.optional(CursorCliToolCallEntry), | |
| 71 | grepToolCall: Schema.optional(CursorCliToolCallEntry), | |
| 72 | globToolCall: Schema.optional(CursorCliToolCallEntry), | |
| 73 | function: Schema.optional(CursorCliFunctionToolCall), | |
| 74 | }); | |
| 75 | export type CursorCliToolCallPayload = typeof CursorCliToolCallPayload.Type; | |
| 76 | ||
| 77 | export const CursorCliTokenUsage = Schema.Struct({ | |
| 78 | inputTokens: Schema.optional(Schema.Number), | |
| 79 | outputTokens: Schema.optional(Schema.Number), | |
| 80 | cacheReadTokens: Schema.optional(Schema.Number), | |
| 81 | cacheWriteTokens: Schema.optional(Schema.Number), | |
| 82 | }); | |
| 83 | export type CursorCliTokenUsage = typeof CursorCliTokenUsage.Type; | |
| 84 | ||
| 85 | const CursorCliTimestampMs = { | |
| 86 | timestamp_ms: Schema.optional(Schema.Number), | |
| 87 | } as const; | |
| 88 | ||
| 89 | export const CursorCliSystemInitEvent = Schema.Struct({ | |
| 90 | type: Schema.Literal("system"), | |
| 91 | subtype: Schema.Literal("init"), | |
| 92 | session_id: CursorCliSessionId, | |
| 93 | apiKeySource: Schema.optional(Schema.String), | |
| 94 | cwd: Schema.optional(Schema.String), | |
| 95 | model: Schema.optional(Schema.String), | |
| 96 | permissionMode: Schema.optional(Schema.String), | |
| 97 | ...CursorCliTimestampMs, | |
| 98 | }); | |
| 99 | export type CursorCliSystemInitEvent = typeof CursorCliSystemInitEvent.Type; | |
| 100 | ||
| 101 | export const CursorCliUserEvent = Schema.Struct({ | |
| 102 | type: Schema.Literal("user"), | |
| 103 | message: CursorCliUserMessage, | |
| 104 | session_id: CursorCliSessionId, | |
| 105 | ...CursorCliTimestampMs, | |
| 106 | }); | |
| 107 | export type CursorCliUserEvent = typeof CursorCliUserEvent.Type; | |
| 108 | ||
| 109 | export const CursorCliAssistantEvent = Schema.Struct({ | |
| 110 | type: Schema.Literal("assistant"), | |
| 111 | message: CursorCliAssistantMessage, | |
| 112 | session_id: CursorCliSessionId, | |
| 113 | ...CursorCliTimestampMs, | |
| 114 | }); | |
| 115 | export type CursorCliAssistantEvent = typeof CursorCliAssistantEvent.Type; | |
| 116 | ||
| 117 | export const CursorCliThinkingDeltaEvent = Schema.Struct({ | |
| 118 | type: Schema.Literal("thinking"), | |
| 119 | subtype: Schema.Literal("delta"), | |
| 120 | text: Schema.String, | |
| 121 | session_id: CursorCliSessionId, | |
| 122 | ...CursorCliTimestampMs, | |
| 123 | }); | |
| 124 | export type CursorCliThinkingDeltaEvent = typeof CursorCliThinkingDeltaEvent.Type; | |
| 125 | ||
| 126 | export const CursorCliThinkingCompletedEvent = Schema.Struct({ | |
| 127 | type: Schema.Literal("thinking"), | |
| 128 | subtype: Schema.Literal("completed"), | |
| 129 | session_id: CursorCliSessionId, | |
| 130 | ...CursorCliTimestampMs, | |
| 131 | }); | |
| 132 | export type CursorCliThinkingCompletedEvent = typeof CursorCliThinkingCompletedEvent.Type; | |
| 133 | ||
| 134 | export const CursorCliToolCallStartedEvent = Schema.Struct({ | |
| 135 | type: Schema.Literal("tool_call"), | |
| 136 | subtype: Schema.Literal("started"), | |
| 137 | call_id: Schema.String, | |
| 138 | tool_call: CursorCliToolCallPayload, | |
| 139 | session_id: CursorCliSessionId, | |
| 140 | ...CursorCliTimestampMs, | |
| 141 | }); | |
| 142 | export type CursorCliToolCallStartedEvent = typeof CursorCliToolCallStartedEvent.Type; | |
| 143 | ||
| 144 | export const CursorCliToolCallCompletedEvent = Schema.Struct({ | |
| 145 | type: Schema.Literal("tool_call"), | |
| 146 | subtype: Schema.Literal("completed"), | |
| 147 | call_id: Schema.String, | |
| 148 | tool_call: CursorCliToolCallPayload, | |
| 149 | session_id: CursorCliSessionId, | |
| 150 | ...CursorCliTimestampMs, | |
| 151 | }); | |
| 152 | export type CursorCliToolCallCompletedEvent = typeof CursorCliToolCallCompletedEvent.Type; | |
| 153 | ||
| 154 | export const CursorCliResultSuccessEvent = Schema.Struct({ | |
| 155 | type: Schema.Literal("result"), | |
| 156 | subtype: Schema.Literal("success"), | |
| 157 | duration_ms: Schema.Number, | |
| 158 | duration_api_ms: Schema.optional(Schema.Number), | |
| 159 | is_error: Schema.Boolean, | |
| 160 | result: Schema.String, | |
| 161 | session_id: CursorCliSessionId, | |
| 162 | request_id: Schema.optional(Schema.String), | |
| 163 | usage: Schema.optional(CursorCliTokenUsage), | |
| 164 | ...CursorCliTimestampMs, | |
| 165 | }); | |
| 166 | export type CursorCliResultSuccessEvent = typeof CursorCliResultSuccessEvent.Type; | |
| 167 | ||
| 168 | export const CursorCliConnectionEvent = Schema.Struct({ | |
| 169 | type: Schema.Literal("connection"), | |
| 170 | subtype: Schema.Literals(["reconnecting", "reconnected"]), | |
| 171 | session_id: Schema.optional(CursorCliSessionId), | |
| 172 | ...CursorCliTimestampMs, | |
| 173 | }); | |
| 174 | export type CursorCliConnectionEvent = typeof CursorCliConnectionEvent.Type; | |
| 175 | ||
| 176 | export const CursorCliRetryEvent = Schema.Struct({ | |
| 177 | type: Schema.Literal("retry"), | |
| 178 | subtype: Schema.Literals(["starting", "resuming"]), | |
| 179 | session_id: Schema.optional(CursorCliSessionId), | |
| 180 | ...CursorCliTimestampMs, | |
| 181 | }); | |
| 182 | export type CursorCliRetryEvent = typeof CursorCliRetryEvent.Type; | |
| 183 | ||
| 184 | export const CursorCliStreamEvent = Schema.Union([ | |
| 185 | CursorCliSystemInitEvent, | |
| 186 | CursorCliUserEvent, | |
| 187 | CursorCliAssistantEvent, | |
| 188 | CursorCliThinkingDeltaEvent, | |
| 189 | CursorCliThinkingCompletedEvent, | |
| 190 | CursorCliToolCallStartedEvent, | |
| 191 | CursorCliToolCallCompletedEvent, | |
| 192 | CursorCliResultSuccessEvent, | |
| 193 | CursorCliConnectionEvent, | |
| 194 | CursorCliRetryEvent, | |
| 195 | ]); | |
| 196 | export type CursorCliStreamEvent = typeof CursorCliStreamEvent.Type; | |
| 197 | ||
| 198 | /** | |
| 199 | * CursorAdapterShape - Service API for the Cursor provider adapter. | |
| 200 | * | |
| 201 | * `provider` is intentionally narrowed to `"cursor"` here. Until contracts add | |
| 202 | * Cursor to `ProviderKind`, this shape is defined via `Omit<...,"provider">`. | |
| 203 | */ | |
| 204 | export interface CursorAdapterShape | |
| 205 | extends Omit<ProviderAdapterShape<ProviderAdapterError>, "provider"> { | |
| 206 | readonly provider: "cursor"; | |
| 207 | } | |
| 208 | ||
| 209 | /** | |
| 210 | * CursorAdapter - Service tag for Cursor provider adapter operations. | |
| 211 | */ | |
| 212 | export class CursorAdapter extends ServiceMap.Service<CursorAdapter, CursorAdapterShape>()( | |
| 213 | "t3/provider/Services/CursorAdapter", | |
| 214 | ) {} |