From bc71b2e854d0af02ea70131ad44ad74f37321588 Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Tue, 27 Oct 2026 23:54:54 -0300 Subject: [PATCH 1/8] feat(cursor): bump SDK to 1.2.0 for ambient MCP, summarize, and maxMode Pass mcpServers on send without wiping settingSources plugins, wire agent.summarize() into compaction items, and honor catalog maxMode variants. Co-authored-by: Cursor --- apps/server/package.json | 2 +- .../orchestrator-mcp-server.md | 7 +- docs/user/cursor.md | 9 +- packages/provider-cursor/package.json | 2 +- .../src/server/CursorAgentSdk.ts | 159 +++++++++ .../src/server/adapter.test.ts | 84 ++++- .../provider-cursor/src/server/adapter.ts | 305 +++++++++++++++++- .../provider-cursor/src/server/driver.test.ts | 15 + packages/provider-cursor/src/server/driver.ts | 13 +- .../src/server/sdkModel.test.ts | 55 ++++ .../provider-cursor/src/server/sdkModel.ts | 57 +++- pnpm-lock.yaml | 54 ++-- pnpm-workspace.yaml | 6 + 13 files changed, 708 insertions(+), 60 deletions(-) create mode 100644 packages/provider-cursor/src/server/sdkModel.test.ts diff --git a/apps/server/package.json b/apps/server/package.json index 25d57720deec4..81404888f941b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.276", - "@cursor/sdk": "1.0.38", + "@cursor/sdk": "1.2.0", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", "@ff-labs/fff-node": "0.11.0", diff --git a/docs/orchestration-v2/orchestrator-mcp-server.md b/docs/orchestration-v2/orchestrator-mcp-server.md index e41f42c8248a3..ee6dbd7e7d057 100644 --- a/docs/orchestration-v2/orchestrator-mcp-server.md +++ b/docs/orchestration-v2/orchestrator-mcp-server.md @@ -98,8 +98,11 @@ server headers or token. ### Cursor Agent SDK V2 Cursor receives the same authenticated HTTP MCP endpoint through the SDK's -`mcpServers` agent and send options. The adapter passes the authorization header -to the SDK but projects only redacted option metadata into protocol diagnostics. +`mcpServers` on `Agent.create` / `Agent.resume` and each `send`. With +`@cursor/sdk` 1.0.39+, send-level servers replace only the create-time inline +set; ambient servers from `settingSources` (`project`, `user`, `team`, `mdm`, +`plugins`) stay available. The adapter passes the authorization header to the +SDK but projects only redacted option metadata into protocol diagnostics. ### Grok ACP V2 diff --git a/docs/user/cursor.md b/docs/user/cursor.md index 6a6b1c8f284b3..94167f440cc52 100644 --- a/docs/user/cursor.md +++ b/docs/user/cursor.md @@ -15,6 +15,12 @@ The adapter currently uses the SDK's local-agent runtime so runs operate in the workspace. Cursor cloud agents need repository and cloud-environment configuration that T3 Code does not expose yet. +Custom Cursor MCP servers and plugins are not configured in T3 Code. They load from the environment +machine's Cursor config when the agent opens: project `.cursor/mcp.json`, user `~/.cursor/mcp.json`, +and installed plugins under `~/.cursor/plugins`, via the SDK `settingSources` layers. OAuth MCP +logins are per workspace folder (`agent mcp login`); a login from the Cursor desktop app is not +reused. After you add or change skills, plugins, or MCP servers, use **Restart agent session**. + ## Sign in Choose **Sign in**, then open the sign-in page and complete it in your browser. T3 Code @@ -36,9 +42,10 @@ The adapter supports: - creating and resuming local Cursor agent threads; - changing the model and model parameters between turns; - assistant text, reasoning, tool activity, plans, and todo streaming; -- thread-scoped T3 Code MCP tools; +- thread-scoped T3 Code MCP tools alongside ambient Cursor MCP/plugins from disk; - image attachments; - interruption, queued app messages, and orchestrator-owned interrupt/restart steering; +- native conversation compaction through `agent.summarize()`; - provider conversation snapshots through `Agent.messages.list()`; - Cursor `task` subagents, projected as read-only child app threads with their tool activity and final result. diff --git a/packages/provider-cursor/package.json b/packages/provider-cursor/package.json index b619c3b61fdb4..90122cbe57e66 100644 --- a/packages/provider-cursor/package.json +++ b/packages/provider-cursor/package.json @@ -49,7 +49,7 @@ "test": "vp test run --config ../../vite.config.ts --dir ." }, "dependencies": { - "@cursor/sdk": "1.0.38", + "@cursor/sdk": "1.2.0", "@napi-rs/keyring": "^1.3.0", "@t3tools/contracts": "workspace:*", "@t3tools/provider-core": "workspace:*", diff --git a/packages/provider-cursor/src/server/CursorAgentSdk.ts b/packages/provider-cursor/src/server/CursorAgentSdk.ts index 7f0decb49af3f..902439337fe66 100644 --- a/packages/provider-cursor/src/server/CursorAgentSdk.ts +++ b/packages/provider-cursor/src/server/CursorAgentSdk.ts @@ -76,6 +76,10 @@ export interface CursorAgentSdkSendInput { readonly onDelta?: (update: InteractionUpdate) => Effect.Effect; } +export interface CursorAgentSdkSummarizeInput { + readonly onDelta?: (update: InteractionUpdate) => Effect.Effect; +} + export interface CursorAgentSdkRun { readonly runId: string; readonly agentId: string; @@ -88,6 +92,9 @@ export interface CursorAgentSdkSession { readonly send: ( input: CursorAgentSdkSendInput, ) => Effect.Effect; + readonly summarize: ( + input?: CursorAgentSdkSummarizeInput, + ) => Effect.Effect; readonly listMessages: Effect.Effect, CursorAgentSdkRunnerError>; readonly close: Effect.Effect; } @@ -124,6 +131,8 @@ export interface CursorAgentSdkLoggedAgentOptions { export interface CursorAgentSdkLoggedSendOptions { readonly model?: SendOptions["model"]; readonly mode?: SendOptions["mode"]; + readonly hasMcpServers?: boolean; + readonly mcpServerNames?: ReadonlyArray; readonly local?: { readonly force?: boolean; readonly hasCustomTools?: boolean; @@ -159,6 +168,13 @@ export type CursorAgentSdkProtocolLogEvent = readonly options: CursorAgentSdkLoggedSendOptions; }; } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "run.summarize"; + }; + } | { readonly direction: "incoming"; readonly stage: "decoded"; @@ -283,9 +299,12 @@ export function loggedCursorSendOptions( if (options === undefined) { return {}; } + const mcpServerNames = + options.mcpServers === undefined ? undefined : Object.keys(options.mcpServers).toSorted(); return { ...(options.model === undefined ? {} : { model: options.model }), ...(options.mode === undefined ? {} : { mode: options.mode }), + ...(mcpServerNames === undefined ? {} : { hasMcpServers: true, mcpServerNames }), ...(options.local === undefined ? {} : { @@ -298,6 +317,25 @@ export function loggedCursorSendOptions( }; } +function summaryMessageToInteractionUpdate(message: { + readonly type: string; + readonly status?: string; + readonly fallback?: boolean; +}): InteractionUpdate | undefined { + if (message.type !== "summary") return undefined; + if (message.status === "running") { + return { type: "summary-started" }; + } + if (message.status === "completed" || message.status === "error") { + return { + type: "summary-completed", + ...(message.status === "error" ? { failed: true } : {}), + ...(message.fallback === true ? { fallback: true } : {}), + }; + } + return undefined; +} + function makeCursorAgentSdkProtocolLogger(input: { readonly nativeEventLogger: ProviderEventLoggers.EventNdjsonLogger | undefined; readonly threadId: ThreadId; @@ -399,6 +437,101 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu }; let abandonedRunRecoveryAvailable = input.operation === "resume"; + const wrapRun = Effect.fnUntraced(function* (input: { + readonly run: Awaited>; + readonly method: string; + readonly onDelta?: (update: InteractionUpdate) => Effect.Effect; + readonly pumpStream?: boolean; + }) { + const context = yield* Effect.context(); + let callbackFailure: { readonly cause: unknown } | undefined; + let callbackChain = Promise.resolve(); + const dispatchUpdate = (update: InteractionUpdate): Promise => { + callbackChain = callbackChain + .then(() => { + if (callbackFailure !== undefined) { + return; + } + return Effect.runPromiseWith(context)( + log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "interaction.update", + runId: input.run.id, + update, + }, + }).pipe(Effect.andThen(input.onDelta?.(update) ?? Effect.void)), + ); + }) + .catch((cause) => { + callbackFailure ??= { cause }; + }); + return callbackChain; + }; + + if (input.pumpStream) { + callbackChain = callbackChain.then(async () => { + for await (const message of input.run.stream()) { + const update = summaryMessageToInteractionUpdate(message); + if (update !== undefined) { + await dispatchUpdate(update); + } + } + }); + } + + return { + runId: input.run.id, + agentId: input.run.agentId, + wait: Effect.tryPromise({ + try: async () => { + const result = await input.run.wait(); + await callbackChain; + if (callbackFailure !== undefined) { + throw callbackFailure.cause; + } + return result; + }, + catch: (cause) => runnerError(cause, `${input.method}.wait`), + }).pipe( + Effect.tap((result) => + log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "run.completed", + result, + }, + }), + ), + ), + cancel: log({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "run.cancel", + runId: input.run.id, + }, + }).pipe( + Effect.andThen( + Effect.tryPromise({ + try: async () => { + try { + await input.run.cancel(); + } catch (cause) { + if (!isCursorCancellationError(cause)) { + throw cause; + } + } + }, + catch: (cause) => runnerError(cause, `${input.method}.cancel`), + }), + ), + ), + } satisfies CursorAgentSdkRun; + }); + return { agentId: agent.agentId, send: Effect.fn("CursorAgentSdkSession.send")(function* (sendInput) { @@ -555,6 +688,32 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu ), } satisfies CursorAgentSdkRun; }), + summarize: Effect.fn("CursorAgentSdkSession.summarize")(function* (summarizeInput) { + yield* log({ + direction: "outgoing", + stage: "decoded", + payload: { type: "run.summarize" }, + }); + const run = yield* Effect.tryPromise({ + try: () => agent.summarize(), + catch: (cause) => runnerError(cause, "run.summarize"), + }); + yield* log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "run.started", + runId: run.id, + agentId: run.agentId, + }, + }); + return yield* wrapRun({ + run, + method: "run.summarize", + ...(summarizeInput?.onDelta === undefined ? {} : { onDelta: summarizeInput.onDelta }), + pumpStream: true, + }); + }), listMessages: log({ direction: "outgoing", stage: "decoded", diff --git a/packages/provider-cursor/src/server/adapter.test.ts b/packages/provider-cursor/src/server/adapter.test.ts index 8283c5c3d6b0d..68a04af06aa3d 100644 --- a/packages/provider-cursor/src/server/adapter.test.ts +++ b/packages/provider-cursor/src/server/adapter.test.ts @@ -66,7 +66,7 @@ describe("CursorAdapterV2", () => { authorizationHeader: "Bearer synthetic-cursor-token", browserToolsAvailable: true, }); - let sentWithMcpOverride = true; + let sentWithMcpOverride = false; const modelSelection = { instanceId, model: "composer-2.5" }; const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ runtimeMode: "full-access", @@ -85,6 +85,20 @@ describe("CursorAdapterV2", () => { agentId: "native-cursor-lifecycle", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => + Effect.succeed({ + agentId: "native-cursor-lifecycle", + runId: "native-cursor-summarize", + wait: Effect.succeed({ + id: "native-cursor-summarize", + requestId: "native-summarize-request", + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + result: "summary", + }), + cancel: Effect.void, + }), send: (input) => Effect.gen(function* () { sentWithMcpOverride = Object.hasOwn(input.options ?? {}, "mcpServers"); @@ -194,7 +208,7 @@ describe("CursorAdapterV2", () => { Stream.takeUntil((event) => event.type === "turn.terminal"), Stream.runCollect, ); - assert.isFalse(sentWithMcpOverride); + assert.isTrue(sentWithMcpOverride); const rows = events.filter((event) => event.type === "subagent.updated"); assert.equal(rows[0]?.subagent.status, "running"); assert.equal(rows[0]?.subagent.model, model ?? null); @@ -348,6 +362,20 @@ describe("CursorAdapterV2", () => { agentId: "native-cursor-failure", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => + Effect.succeed({ + agentId: "native-cursor-failure", + runId: "native-cursor-summarize", + wait: Effect.succeed({ + id: "native-cursor-summarize", + requestId: "native-summarize-request", + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + result: "summary", + }), + cancel: Effect.void, + }), send: (input) => Effect.gen(function* () { for (const update of updates) { @@ -443,12 +471,13 @@ describe("CursorAdapterV2", () => { ), ); - it.effect("fails standalone SDK transport diagnostics and sends compaction as /compress", () => + it.effect("fails standalone SDK transport diagnostics and compacts with summarize", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "cursor-v2-errors-" }); const sentMessages: Array = []; let sendCalls = 0; + let summarizeCalls = 0; const instanceId = ProviderInstanceId.make("cursor"); const threadId = ThreadId.make("cursor-transport-error-thread"); const modelSelection = { instanceId, model: "composer-2.5" }; @@ -469,6 +498,23 @@ describe("CursorAdapterV2", () => { agentId: "native-cursor-error", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => + Effect.sync(() => { + summarizeCalls += 1; + return { + agentId: "native-cursor-error", + runId: `native-cursor-summarize-${summarizeCalls}`, + wait: Effect.succeed({ + id: `native-cursor-summarize-${summarizeCalls}`, + requestId: `native-summarize-request-${summarizeCalls}`, + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + result: "Compacted conversation summary.", + }), + cancel: Effect.void, + }; + }), send: (input) => Effect.sync(() => { sendCalls += 1; @@ -580,11 +626,20 @@ describe("CursorAdapterV2", () => { attachments: [], }, }); - yield* runtime.events.pipe( - Stream.filter((event) => event.type === "turn.terminal"), - Stream.runHead, + const compactEvents = yield* runtime.events.pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + ); + assert.equal(summarizeCalls, 1); + assert.equal(sentMessages.length, 1); + assert.isTrue( + compactEvents.some( + (event) => + event.type === "turn_item.updated" && + event.turnItem.type === "compaction" && + event.turnItem.status === "completed", + ), ); - assert.equal(sentMessages[1], "/compress"); }).pipe( Effect.scoped, Effect.provide( @@ -811,6 +866,20 @@ describe("CursorAdapterV2", () => { agentId: "native-cursor-search", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => + Effect.succeed({ + agentId: "native-cursor-search", + runId: "native-cursor-summarize", + wait: Effect.succeed({ + id: "native-cursor-summarize", + requestId: "native-summarize-request", + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + result: "summary", + }), + cancel: Effect.void, + }), send: (input) => Effect.gen(function* () { for (const update of updates) { @@ -1035,6 +1104,7 @@ describe("CursorAdapterV2", () => { agentId: "native-cursor-custom-model", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => Effect.die("unexpected Cursor summarize"), send: (sendInput) => checkModel("run.start", sendInput.options?.model).pipe( Effect.andThen(Effect.die("unexpected Cursor run")), diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index 186334fe5c913..fc06fb35a0ce3 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -4,6 +4,7 @@ import type { InteractionUpdate, McpServerConfig, RunResult, + SDKModel, SDKUserMessage, SettingSource, ToolCall, @@ -308,11 +309,12 @@ export function makeCursorAgentOptions(input: { readonly threadId: ThreadId; readonly mcpSession: McpProviderSession.McpProviderSessionConfig | undefined; readonly platform: NodeJS.Platform; + readonly sdkModels?: ReadonlyArray>; }): AgentOptions { const policy = cursorRuntimeAgentPolicy(input.runtimePolicy, input.platform); const mcpServers = cursorMcpServers(input.mcpSession); return { - model: cursorSdkModelSelection(input.modelSelection), + model: cursorSdkModelSelection(input.modelSelection, input.sdkModels), name: `T3 Code ${input.threadId}`, mode: input.runtimePolicy.interactionMode === "plan" ? "plan" : "agent", ...(input.apiKey === undefined ? {} : { apiKey: input.apiKey }), @@ -848,6 +850,15 @@ interface ActiveCursorTextStream { nextSegment: number; } +interface ActiveCursorCompaction { + readonly nativeItemId: string; + readonly startedAt: DateTime.Utc; + summary?: string; + failed?: boolean; + fallback?: boolean; + status: "running" | "completed" | "failed"; +} + interface ActiveCursorTurn { readonly input: ProviderAdapter.ProviderAdapterV2TurnInput; readonly run: CursorAgentSdk.CursorAgentSdkRun; @@ -860,6 +871,8 @@ interface ActiveCursorTurn { readonly assistant: ActiveCursorTextStream; assistantReply: CursorTransportFailure; readonly reasoning: ActiveCursorTextStream; + /** Set when this turn is a native summarize/compaction run. */ + compaction: ActiveCursorCompaction | null; interrupted: boolean; finalized: boolean; /** The agent's latest turn ended and no new work has started since. */ @@ -876,6 +889,7 @@ export interface CursorAdapterV2Options { readonly settings: CursorSettings; readonly environment: NodeJS.ProcessEnv; readonly getModels?: () => ReadonlyArray; + readonly getSdkModels?: () => ReadonlyArray>; } export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( @@ -1904,6 +1918,97 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( return candidates.filter((value): value is string => typeof value === "string").join(""); }; + const emitCompactionItem = Effect.fnUntraced(function* (context: ActiveCursorTurn) { + const compaction = context.compaction; + if (compaction === null) return; + const now = yield* DateTime.now; + const ordinal = yield* resolveItemOrdinal(context, compaction.nativeItemId); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CursorAgentSdk.CURSOR_PROVIDER, + turnItem: { + id: idAllocator.derive.turnItemFromProviderItem({ + driver: CursorAgentSdk.CURSOR_PROVIDER, + nativeItemId: compaction.nativeItemId, + }), + threadId: context.input.threadId, + runId: context.input.runId, + nodeId: context.input.rootNodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: { + driver: CursorAgentSdk.CURSOR_PROVIDER, + nativeId: compaction.nativeItemId, + strength: "weak", + }, + parentItemId: null, + ordinal, + type: "compaction", + driver: CursorAgentSdk.CURSOR_PROVIDER, + status: compaction.status, + title: compaction.failed + ? "Context compaction failed" + : compaction.fallback + ? "Context trimmed" + : "Context compacted", + startedAt: compaction.startedAt, + completedAt: compaction.status === "running" ? null : now, + updatedAt: now, + ...(compaction.summary === undefined ? {} : { summary: compaction.summary }), + }, + }); + }); + + const handleSummaryUpdate = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + update: Extract< + InteractionUpdate, + { type: "summary-started" | "summary" | "summary-completed" } + >, + ) { + if (update.type === "summary-started") { + if (context.compaction === null) { + context.compaction = { + nativeItemId: `${context.providerTurnId}:compaction`, + startedAt: yield* DateTime.now, + status: "running", + }; + } else { + context.compaction.status = "running"; + } + yield* emitCompactionItem(context); + return; + } + if (update.type === "summary") { + if (context.compaction === null) { + context.compaction = { + nativeItemId: `${context.providerTurnId}:compaction`, + startedAt: yield* DateTime.now, + status: "running", + summary: update.summary, + }; + } else { + context.compaction.summary = update.summary; + } + yield* emitCompactionItem(context); + return; + } + if (context.compaction === null) { + context.compaction = { + nativeItemId: `${context.providerTurnId}:compaction`, + startedAt: yield* DateTime.now, + status: update.failed === true ? "failed" : "completed", + ...(update.failed === true ? { failed: true } : {}), + ...(update.fallback === true ? { fallback: true } : {}), + }; + } else { + context.compaction.failed = update.failed === true; + context.compaction.fallback = update.fallback === true; + context.compaction.status = update.failed === true ? "failed" : "completed"; + } + yield* emitCompactionItem(context); + }); + const handleInteractionUpdate = Effect.fnUntraced(function* ( context: ActiveCursorTurn, update: InteractionUpdate, @@ -1990,6 +2095,11 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( }); return; } + case "summary-started": + case "summary": + case "summary-completed": + yield* handleSummaryUpdate(context, update); + return; default: return; } @@ -2157,6 +2267,7 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( platform, threadId: openInput.threadId, mcpSession: yield* mcpSessions.read(openInput.threadId), + sdkModels: adapterOptions.getSdkModels?.() ?? [], }); if ( platform === "win32" && @@ -2288,13 +2399,17 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( const message = yield* resolveUserMessage(turnInput, mcpServers); const pendingUpdates: Array = []; let context: ActiveCursorTurn | null = null; - // MCP send overrides replace the ambient servers loaded when the agent opens. + // Create-time and send-time mcpServers merge with settingSources (SDK >= 1.0.39). const sdkRun = yield* agent.session .send({ message, options: { - model: cursorSdkModelSelection(turnInput.modelSelection), + model: cursorSdkModelSelection( + turnInput.modelSelection, + adapterOptions.getSdkModels?.() ?? [], + ), mode: turnInput.runtimePolicy.interactionMode === "plan" ? "plan" : "agent", + ...(mcpServers === undefined ? {} : { mcpServers }), }, onDelta: (update) => { if (context === null) { @@ -2330,6 +2445,7 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( current: null, nextSegment: 0, }, + compaction: null, interrupted: false, finalized: false, turnEnded: false, @@ -2361,7 +2477,17 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( yield* sdkRun.wait.pipe( Effect.flatMap((result) => Effect.gen(function* () { - if ( + if (context !== null && context.compaction !== null) { + if (result.result !== undefined && result.result.length > 0) { + context.compaction.summary = result.result; + } + if (context.compaction.status === "running") { + context.compaction.status = + result.status === "finished" ? "completed" : "failed"; + context.compaction.failed = result.status !== "finished"; + yield* emitCompactionItem(context); + } + } else if ( context !== null && context.assistant.nextSegment === 0 && // Text from a resume that made no progress is not the turn's answer. @@ -2548,11 +2674,172 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( ), ), startTurn, - compactThread: (turnInput) => - startTurn({ - ...turnInput, - message: { ...turnInput.message, text: "/compress" }, - }), + compactThread: Effect.fn("CursorAdapterV2.compactThread")( + function* (turnInput: ProviderAdapter.ProviderAdapterV2TurnInput) { + const current = yield* Ref.get(activeTurn); + if (current !== null) { + return yield* new ProviderAdapter.ProviderAdapterProtocolError({ + driver: CursorAgentSdk.CURSOR_PROVIDER, + detail: `Cursor provider turn ${current.providerTurnId} is still active.`, + reason: "previous_turn_active", + }); + } + const agentId = nativeThreadId(turnInput.providerThread); + const agent = yield* openAgent({ + operation: "resume", + agentId, + threadId: turnInput.threadId, + modelSelection: turnInput.modelSelection, + runtimePolicy: turnInput.runtimePolicy, + }); + const pendingUpdates: Array = []; + let context: ActiveCursorTurn | null = null; + const sdkRun = yield* agent.session + .summarize({ + onDelta: (update) => { + if (context === null) { + return Effect.sync(() => { + pendingUpdates.push(update); + }); + } + return handleInteractionUpdate(context, update); + }, + }) + .pipe(Effect.mapError(classifyCursorModelRejection(turnInput.modelSelection))); + const startedAt = yield* DateTime.now; + const completed = yield* Deferred.make(); + const providerTurnId = idAllocator.derive.providerTurn({ + driver: CursorAgentSdk.CURSOR_PROVIDER, + nativeTurnId: sdkRun.runId, + }); + context = { + input: turnInput, + run: sdkRun, + providerTurnId, + startedAt, + completed, + itemOrdinals: new Map(), + tools: new Map(), + subagents: new Map(), + assistant: { + current: null, + nextSegment: 0, + }, + assistantReply: new CursorTransportFailure(), + reasoning: { + current: null, + nextSegment: 0, + }, + compaction: { + nativeItemId: `${providerTurnId}:compaction`, + startedAt, + status: "running", + }, + interrupted: false, + finalized: false, + turnEnded: false, + }; + yield* Ref.set(activeTurn, context); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver: CursorAgentSdk.CURSOR_PROVIDER, + providerTurn: providerTurnPayload({ + context, + status: "running", + completedAt: null, + }), + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: CursorAgentSdk.CURSOR_PROVIDER, + providerThread: { + ...turnInput.providerThread, + providerSessionId: session.id, + status: "active", + updatedAt: startedAt, + }, + }); + yield* emitCompactionItem(context); + for (const update of pendingUpdates) { + yield* handleInteractionUpdate(context, update); + } + yield* sdkRun.wait.pipe( + Effect.flatMap((result) => + Effect.gen(function* () { + if (context === null) return; + if (result.result !== undefined && result.result.length > 0) { + context.compaction!.summary = result.result; + } + if (context.compaction!.status === "running") { + context.compaction!.status = + result.status === "finished" ? "completed" : "failed"; + context.compaction!.failed = result.status !== "finished"; + yield* emitCompactionItem(context); + } + yield* finalizeTurn({ + context, + status: terminalStatus(context, result), + ...(result.status === "error" + ? { + failure: makeProviderFailure({ + cause: result.error, + message: result.error?.message, + class: "provider_error", + }), + } + : {}), + }); + }), + ), + Effect.catch((cause) => + Effect.gen(function* () { + if (context !== null) { + if (context.compaction !== null && context.compaction.status === "running") { + context.compaction.status = "failed"; + context.compaction.failed = true; + yield* emitCompactionItem(context); + } + yield* finalizeTurn({ + context, + status: context.interrupted ? "interrupted" : "failed", + ...(context.interrupted + ? {} + : { + failure: makeProviderFailure({ + cause, + message: CursorAgentSdk.isCursorAgentSdkRunnerError(cause) + ? CursorAgentSdk.cursorSdkFailureMessage(cause) + : undefined, + class: "transport_error", + }), + }), + }); + } + yield* Effect.logWarning("orchestration-v2.cursor-summarize-failed", { + providerSessionId: input.providerSessionId, + providerThreadId: turnInput.providerThread.id, + providerTurnId, + cause, + }); + }), + ), + Effect.forkIn(sessionScope), + ); + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapter.ProviderAdapterTurnStartError({ + driver: CursorAgentSdk.CURSOR_PROVIDER, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ), + ), steerTurn: (turnInput) => Effect.fail( new ProviderAdapter.ProviderAdapterSteerRunUnsupportedError({ diff --git a/packages/provider-cursor/src/server/driver.test.ts b/packages/provider-cursor/src/server/driver.test.ts index 5a3202fd20e4d..7ec192877b355 100644 --- a/packages/provider-cursor/src/server/driver.test.ts +++ b/packages/provider-cursor/src/server/driver.test.ts @@ -116,6 +116,7 @@ it.layer(layerTest)("CursorDriver", (it) => { return { agentId: "browser-auth-agent", listMessages: Effect.succeed([]), + summarize: () => Effect.die("This test only opens a session"), send: () => Effect.die("This test only opens a session"), close: Effect.sync(() => { closed += 1; @@ -212,6 +213,20 @@ it.layer(layerTest)("CursorDriver", (it) => { agentId: "native-cursor-window", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => + Effect.succeed({ + agentId: "native-cursor-window", + runId: "native-cursor-window-summarize", + cancel: Effect.void, + wait: Effect.succeed({ + id: "native-cursor-window-summarize", + requestId: "native-cursor-window-summarize", + status: "finished" as const, + model: { id: "claude-opus-5-5" }, + durationMs: 1, + result: "summary", + }), + }), send: (input) => Effect.sync(() => { sentModels.push(input.options?.model); diff --git a/packages/provider-cursor/src/server/driver.ts b/packages/provider-cursor/src/server/driver.ts index b61301390d613..5948688ec2c5e 100644 --- a/packages/provider-cursor/src/server/driver.ts +++ b/packages/provider-cursor/src/server/driver.ts @@ -1,3 +1,4 @@ +import type { SDKModel } from "@cursor/sdk"; import { ProviderDriverKind, ProviderSetupError, @@ -7,6 +8,7 @@ import { CursorSettings } from "../settings.ts"; import * as Effect from "effect/Effect"; import * as Crypto from "effect/Crypto"; import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as HttpClient from "effect/http/HttpClient"; @@ -165,11 +167,13 @@ export const CursorDriver: ProviderDriver< }); let catalogModels: ReadonlyArray = []; + let catalogSdkModels: ReadonlyArray = []; const orchestrationAdapter = yield* makeCursorAdapterV2({ instanceId, settings: effectiveConfig, environment: processEnv, getModels: () => catalogModels, + getSdkModels: () => catalogSdkModels, }).pipe( Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, { ...sdkRunner, @@ -252,11 +256,18 @@ export const CursorDriver: ProviderDriver< Effect.provideService(CursorKeychain.CursorKeychain, keychain), Effect.map(stampSnapshot), Effect.tap((provider) => - Effect.sync(() => { + Effect.gen(function* () { // A failed check lists only custom models, so keep the last loaded catalog. if (provider.models.some((model) => !model.isCustom)) { catalogModels = provider.models; } + const apiKey = yield* auth.readApiKey.pipe(Effect.orElseSucceed(() => undefined)); + if (apiKey === undefined || apiKey.trim() === "") return; + const catalog = yield* CursorSdkCatalog.CursorSdkCatalog; + const snapshot = yield* catalog.read(apiKey).pipe(Effect.option); + if (Option.isSome(snapshot)) { + catalogSdkModels = snapshot.value.models; + } }), ), Effect.provide(CursorSdkCatalog.layer), diff --git a/packages/provider-cursor/src/server/sdkModel.test.ts b/packages/provider-cursor/src/server/sdkModel.test.ts new file mode 100644 index 0000000000000..a19eaa94d2d39 --- /dev/null +++ b/packages/provider-cursor/src/server/sdkModel.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "@effect/vitest"; +import { ProviderInstanceId } from "@t3tools/contracts"; +import { cursorSdkMaxModeForParams, cursorSdkModelSelection } from "./sdkModel.ts"; + +describe("cursorSdkModelSelection", () => { + const instanceId = ProviderInstanceId.make("cursor"); + + it("sets maxMode when every matching catalog variant requires it", () => { + const selection = cursorSdkModelSelection( + { + instanceId, + model: "claude-opus-4-8", + options: [{ id: "contextWindow", value: "1m" }], + }, + [ + { + id: "claude-opus-4-8", + variants: [ + { + displayName: "1M", + isMaxMode: true, + params: [{ id: "context", value: "1m" }], + }, + ], + }, + ], + ); + expect(selection).toEqual({ + id: "claude-opus-4-8", + params: [{ id: "context", value: "1m" }], + maxMode: true, + }); + }); + + it("omits maxMode when a matching variant can run without it", () => { + expect( + cursorSdkMaxModeForParams( + { + variants: [ + { + displayName: "1M", + isMaxMode: true, + params: [{ id: "context", value: "1m" }], + }, + { + displayName: "1M standard", + params: [{ id: "context", value: "1m" }], + }, + ], + }, + [{ id: "context", value: "1m" }], + ), + ).toBe(false); + }); +}); diff --git a/packages/provider-cursor/src/server/sdkModel.ts b/packages/provider-cursor/src/server/sdkModel.ts index fec0bafc45eee..a3125ed7628bb 100644 --- a/packages/provider-cursor/src/server/sdkModel.ts +++ b/packages/provider-cursor/src/server/sdkModel.ts @@ -1,4 +1,9 @@ -import type { ModelSelection as CursorSdkModelSelection, ModelParameterValue } from "@cursor/sdk"; +import type { + ModelSelection as CursorSdkModelSelection, + ModelParameterValue, + ModelVariant, + SDKModel, +} from "@cursor/sdk"; import type { ModelSelection, ServerProviderModel } from "@t3tools/contracts"; import { getProviderOptionCurrentValue } from "@t3tools/shared/model"; @@ -55,16 +60,46 @@ export function cursorContextWindowTokens( return Number.isSafeInteger(tokens) && tokens > 0 ? tokens : undefined; } -export function cursorSdkModelSelection(modelSelection: ModelSelection): CursorSdkModelSelection { +function variantMatchesParams( + variant: ModelVariant, + params: ReadonlyArray | undefined, +): boolean { + if (params === undefined || params.length === 0) { + return variant.isDefault === true || variant.params.length === 0; + } + return params.every((param) => + variant.params.some((entry) => entry.id === param.id && entry.value === param.value), + ); +} + +/** True when every catalog variant consistent with `params` requires max mode. */ +export function cursorSdkMaxModeForParams( + model: Pick, + params: ReadonlyArray | undefined, +): boolean { + const variants = model.variants ?? []; + const matching = variants.filter((variant) => variantMatchesParams(variant, params)); + return matching.length > 0 && matching.every((variant) => variant.isMaxMode === true); +} + +export function cursorSdkModelSelection( + modelSelection: ModelSelection, + sdkModels?: ReadonlyArray>, +): CursorSdkModelSelection { + const id = modelSelection.model === "auto" ? "default" : modelSelection.model; + const params = + modelSelection.options === undefined || modelSelection.options.length === 0 + ? undefined + : modelSelection.options.map((option): ModelParameterValue => ({ + id: cursorSdkParameterId(option.id), + value: String(option.value), + })); + const catalogModel = sdkModels?.find((model) => model.id === id); + const maxMode = + catalogModel === undefined ? false : cursorSdkMaxModeForParams(catalogModel, params); return { - id: modelSelection.model === "auto" ? "default" : modelSelection.model, - ...(modelSelection.options === undefined || modelSelection.options.length === 0 - ? {} - : { - params: modelSelection.options.map((option): ModelParameterValue => ({ - id: cursorSdkParameterId(option.id), - value: String(option.value), - })), - }), + id, + ...(params === undefined ? {} : { params }), + ...(maxMode ? { maxMode: true } : {}), }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21c84a0f92515..2aa0a0b7d36a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -826,8 +826,8 @@ importers: specifier: ^0.3.276 version: 0.3.276(@anthropic-ai/sdk@0.93.0(zod@4.6.5))(@modelcontextprotocol/sdk@1.29.0(supports-color@8.1.1)(zod@4.6.5))(zod@4.6.5) '@cursor/sdk': - specifier: 1.0.38 - version: 1.0.38(zod@4.6.5) + specifier: 1.2.0 + version: 1.2.0(zod@4.6.5) '@effect/platform-node': specifier: 4.0.5 version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) @@ -1649,8 +1649,8 @@ importers: packages/provider-cursor: dependencies: '@cursor/sdk': - specifier: 1.0.38 - version: 1.0.38(zod@4.6.5) + specifier: 1.2.0 + version: 1.2.0(zod@4.6.5) '@napi-rs/keyring': specifier: ^1.3.0 version: 1.3.0 @@ -3388,38 +3388,38 @@ packages: resolution: {integrity: sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==} engines: {node: '>=20.19.0'} - '@cursor/sdk-darwin-arm64@1.0.38': - resolution: {integrity: sha512-pXxj4pjbIoDo6ZjgksSf4VTD4EJz6BGDKvFjDVVUMg0PmLCVbceOnp0pqfRO8SVpdUSE45GFrXuLnSZgLag/xw==} + '@cursor/sdk-darwin-arm64@1.2.0': + resolution: {integrity: sha512-UkZ/RdxKCpOQ85nRgzSq+g0h+gIw36/lCEvQvomhs5EkSFZW3OwbeEzkAU3UYglZ7t3RNHj3mc5C5cdDtHguvw==} cpu: [arm64] os: [darwin] hasBin: true - '@cursor/sdk-darwin-x64@1.0.38': - resolution: {integrity: sha512-rutfTMq1f9WZckPFH8R5bzNzHaZg7I/TijVUkfD3GbqIqqc5OKHzdTMqZf+AFAqRDQccGJn4GLWcnQ9wTwIVNA==} + '@cursor/sdk-darwin-x64@1.2.0': + resolution: {integrity: sha512-mJTFdX+qv9dHAm59tT7uq7ci7+BDvFoNk4gdF2zGp9SLULicY+FdFD64p1qLen1bX5Y2soZaX5mWBy7BfObjcg==} cpu: [x64] os: [darwin] hasBin: true - '@cursor/sdk-linux-arm64@1.0.38': - resolution: {integrity: sha512-CqmUnM8W8W2mKucSrdHO0kn4ems82siWuo7kvVJvXCtPkaziVNC++dro5h9hSh2oVgRbB91sWQ4lzsHVIDpkvA==} + '@cursor/sdk-linux-arm64@1.2.0': + resolution: {integrity: sha512-Rs+cd5snuhxMAHkpqRfEGPB1qAsfqSOYJh5SOHZTOWy2LkRVO62WHwd4x80C2emj+dosTV5WAcuF2kMKKcrDJg==} cpu: [arm64] os: [linux] hasBin: true - '@cursor/sdk-linux-x64@1.0.38': - resolution: {integrity: sha512-RL6OJTAF3oozMG+HNtykySQ5BOP94yJNWP65wZ2yzMLTAW9ao80++OnaWJya1lndLacHLYP+QZT4igm5TT+BWQ==} + '@cursor/sdk-linux-x64@1.2.0': + resolution: {integrity: sha512-ainsGAFtkWTBZrLABp82Hvfv7VB/FxmZHE42qMVotFTRDwOY2iduvhSig910bVxv+kw3S00Srmu/9CxklLwhbw==} cpu: [x64] os: [linux] hasBin: true - '@cursor/sdk-win32-x64@1.0.38': - resolution: {integrity: sha512-IUfuCk/S/qzEB+tfT4yWH6iSMz56Lvakn1zNjdk+oAsZ7RJWtWkZcea6eqMSRB1WLmTeQG0uetXKX330w+xI0A==} + '@cursor/sdk-win32-x64@1.2.0': + resolution: {integrity: sha512-p3rkyta6R/nXIukd3/ifJq32otkkGoUi41T2jxnHbPHMhzE2F8OuJQGBVmORuQLMr+bIGGVZmCxertgs4WgzcQ==} cpu: [x64] os: [win32] hasBin: true - '@cursor/sdk@1.0.38': - resolution: {integrity: sha512-8UwPKaprIXvlVR71yHyNNTyFwZCOzfHW7W+FKjPdDS9Ym9FDZc51Jwsu9lAhxHf7n0US3t0pybkDaNFdldrQ+A==} + '@cursor/sdk@1.2.0': + resolution: {integrity: sha512-Nqm2U0zTWrYEDCv3Rdwsfa1gYZWsg+JnY+I2F8BwWSAr3FNo0TZWwyHWoWngZgriF1w02fJ4gjJWaul9zysvgQ==} engines: {node: '>=22.13'} peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -13367,22 +13367,22 @@ snapshots: '@csstools/css-tokenizer@4.0.1': {} - '@cursor/sdk-darwin-arm64@1.0.38': + '@cursor/sdk-darwin-arm64@1.2.0': optional: true - '@cursor/sdk-darwin-x64@1.0.38': + '@cursor/sdk-darwin-x64@1.2.0': optional: true - '@cursor/sdk-linux-arm64@1.0.38': + '@cursor/sdk-linux-arm64@1.2.0': optional: true - '@cursor/sdk-linux-x64@1.0.38': + '@cursor/sdk-linux-x64@1.2.0': optional: true - '@cursor/sdk-win32-x64@1.0.38': + '@cursor/sdk-win32-x64@1.2.0': optional: true - '@cursor/sdk@1.0.38(zod@4.6.5)': + '@cursor/sdk@1.2.0(zod@4.6.5)': dependencies: '@bufbuild/protobuf': 1.10.0 '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) @@ -13390,11 +13390,11 @@ snapshots: '@statsig/js-client': 3.31.0 zod: 4.6.5 optionalDependencies: - '@cursor/sdk-darwin-arm64': 1.0.38 - '@cursor/sdk-darwin-x64': 1.0.38 - '@cursor/sdk-linux-arm64': 1.0.38 - '@cursor/sdk-linux-x64': 1.0.38 - '@cursor/sdk-win32-x64': 1.0.38 + '@cursor/sdk-darwin-arm64': 1.2.0 + '@cursor/sdk-darwin-x64': 1.2.0 + '@cursor/sdk-linux-arm64': 1.2.0 + '@cursor/sdk-linux-x64': 1.2.0 + '@cursor/sdk-win32-x64': 1.2.0 '@date-fns/tz@1.5.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a07ad2b008d79..2cf51543c4f4d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -227,6 +227,12 @@ minimumReleaseAgeExclude: - "@pierre/highlights@0.2.0" - "@pierre/theming@1.0.2" - agent-device@0.21.32 + - "@cursor/sdk-darwin-arm64@1.2.0" + - "@cursor/sdk-darwin-x64@1.2.0" + - "@cursor/sdk-linux-arm64@1.2.0" + - "@cursor/sdk-linux-x64@1.2.0" + - "@cursor/sdk-win32-x64@1.2.0" + - "@cursor/sdk@1.2.0" overrides: # Pin every patched package, including transitive dependencies, so lockfile-free From aa15082230b20f282d814c51cc86224ce7dcf060 Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Tue, 27 Oct 2026 23:58:32 -0300 Subject: [PATCH 2/8] fix(server): teach Cursor SDK replay testkit summarize Session mocks need agent.summarize() after the 1.2.0 bump so server typecheck stays green. Co-authored-by: Cursor --- .../Adapters/CursorAdapterV2.testkit.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts index f324de430b716..6c80a5330d760 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts @@ -289,7 +289,9 @@ export function makeCursorAgentSdkReplayRunner( const waitForRun = ( runId: string, - sendInput: CursorAgentSdk.CursorAgentSdkSendInput, + runInput: { + readonly onDelta?: (update: InteractionUpdate) => Effect.Effect; + }, ): Effect.Effect => Effect.gen(function* () { while (true) { @@ -365,7 +367,7 @@ export function makeCursorAgentSdkReplayRunner( ); } advance(); - yield* (sendInput.onDelta?.(frame.update) ?? Effect.void).pipe( + yield* (runInput.onDelta?.(frame.update) ?? Effect.void).pipe( Effect.mapError((cause) => recordFailure( new CursorReplayRuntimeError({ @@ -447,6 +449,34 @@ export function makeCursorAgentSdkReplayRunner( }, catch: (cause) => replayRunnerError(transcript, cause, "replay.session.send"), }), + summarize: (summarizeInput) => + Effect.try({ + try: () => { + assertOutbound({ + type: "run.summarize", + }); + const started = consumeInbound("run.started"); + const run: CursorAgentSdk.CursorAgentSdkRun = { + runId: started.runId, + agentId: started.agentId, + wait: waitForRun(started.runId, summarizeInput ?? {}).pipe( + Effect.mapError((cause) => + replayRunnerError(transcript, cause, "replay.run.wait"), + ), + ), + cancel: Effect.try({ + try: () => + assertOutbound({ + type: "run.cancel", + runId: started.runId, + }), + catch: (cause) => replayRunnerError(transcript, cause, "replay.run.cancel"), + }), + }; + return run; + }, + catch: (cause) => replayRunnerError(transcript, cause, "replay.session.summarize"), + }), listMessages: Effect.try({ try: () => { assertOutbound({ From ddcee48d72be1ec4f28d1139e4a863ad0aeeeeab Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Wed, 28 Oct 2026 00:18:36 -0300 Subject: [PATCH 3/8] fix(cursor): unblock summarize stream and reuse timed catalog probe Keep the SDK summary stream pump off callbackChain so onDelta cannot deadlock wait, and return sdkModels from the timed status check so refresh no longer awaits a second unbounded catalog.read. Document that OAuth plugin MCP needs agent mcp login for the T3 project folder because local SDK runs reject interactive auth. Co-authored-by: Cursor --- docs/user/cursor.md | 11 +- .../src/server/CursorAgentSdk.ts | 27 ++-- packages/provider-cursor/src/server/driver.ts | 58 ++++--- .../provider-cursor/src/server/status.test.ts | 12 +- packages/provider-cursor/src/server/status.ts | 152 ++++++++++-------- 5 files changed, 150 insertions(+), 110 deletions(-) diff --git a/docs/user/cursor.md b/docs/user/cursor.md index 94167f440cc52..3882fe0834b86 100644 --- a/docs/user/cursor.md +++ b/docs/user/cursor.md @@ -17,9 +17,14 @@ not expose yet. Custom Cursor MCP servers and plugins are not configured in T3 Code. They load from the environment machine's Cursor config when the agent opens: project `.cursor/mcp.json`, user `~/.cursor/mcp.json`, -and installed plugins under `~/.cursor/plugins`, via the SDK `settingSources` layers. OAuth MCP -logins are per workspace folder (`agent mcp login`); a login from the Cursor desktop app is not -reused. After you add or change skills, plugins, or MCP servers, use **Restart agent session**. +and installed plugins under `~/.cursor/plugins`, via the SDK `settingSources` layers. Plugin tools +can appear without a login; OAuth MCP servers still need credentials. Run `agent mcp login ` +with the cwd set to the T3 project root (tokens land in +`~/.cursor/projects//mcp-auth.json`). Cursor desktop Connections auth does not write +that file, and the local SDK cannot complete interactive MCP OAuth itself—auth attempts fail with +messages such as "MCP authentication is not supported in local SDK runs" or that the auth server +does not support dynamic client registration. After you add or change skills, plugins, or MCP +servers, or after a successful CLI login, use **Restart agent session**. ## Sign in diff --git a/packages/provider-cursor/src/server/CursorAgentSdk.ts b/packages/provider-cursor/src/server/CursorAgentSdk.ts index 902439337fe66..cf7dff71ca97d 100644 --- a/packages/provider-cursor/src/server/CursorAgentSdk.ts +++ b/packages/provider-cursor/src/server/CursorAgentSdk.ts @@ -470,16 +470,21 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu return callbackChain; }; - if (input.pumpStream) { - callbackChain = callbackChain.then(async () => { - for await (const message of input.run.stream()) { - const update = summaryMessageToInteractionUpdate(message); - if (update !== undefined) { - await dispatchUpdate(update); - } - } - }); - } + // Keep the stream pump off callbackChain — awaiting dispatchUpdate from inside a + // callbackChain task deadlocks when the next link waits for that same task to finish. + const pumpDone = + input.pumpStream === true + ? (async () => { + for await (const message of input.run.stream()) { + const update = summaryMessageToInteractionUpdate(message); + if (update !== undefined) { + await dispatchUpdate(update); + } + } + })().catch((cause) => { + callbackFailure ??= { cause }; + }) + : Promise.resolve(); return { runId: input.run.id, @@ -487,7 +492,7 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu wait: Effect.tryPromise({ try: async () => { const result = await input.run.wait(); - await callbackChain; + await Promise.all([callbackChain, pumpDone]); if (callbackFailure !== undefined) { throw callbackFailure.cause; } diff --git a/packages/provider-cursor/src/server/driver.ts b/packages/provider-cursor/src/server/driver.ts index 5948688ec2c5e..88de867cc7f62 100644 --- a/packages/provider-cursor/src/server/driver.ts +++ b/packages/provider-cursor/src/server/driver.ts @@ -8,7 +8,6 @@ import { CursorSettings } from "../settings.ts"; import * as Effect from "effect/Effect"; import * as Crypto from "effect/Crypto"; import * as FileSystem from "effect/FileSystem"; -import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as HttpClient from "effect/http/HttpClient"; @@ -229,24 +228,29 @@ export const CursorDriver: ProviderDriver< }, auth.usesApiKey ? "api-key" : "browser", ).pipe( - Effect.filterOrElse( - (snapshot) => - !( - effectiveConfig.enabled && - snapshot.installed && - snapshot.auth.status === "authenticated" - ), - (snapshot) => - host.settings.get.pipe( - Effect.flatMap((settings) => - readCursorUsageLimits( - effectiveConfig, - { ...processEnv, CURSOR_API_KEY: apiKey }, - settings.cursorKeychainUsageEnabled, + Effect.flatMap(({ provider: statusProvider, sdkModels }) => + Effect.succeed(statusProvider).pipe( + Effect.filterOrElse( + (snapshot) => + !( + effectiveConfig.enabled && + snapshot.installed && + snapshot.auth.status === "authenticated" + ), + (snapshot) => + host.settings.get.pipe( + Effect.flatMap((settings) => + readCursorUsageLimits( + effectiveConfig, + { ...processEnv, CURSOR_API_KEY: apiKey }, + settings.cursorKeychainUsageEnabled, + ), + ), + Effect.map((usageLimits) => ({ ...snapshot, usageLimits })), ), - ), - Effect.map((usageLimits) => ({ ...snapshot, usageLimits })), ), + Effect.map((provider) => ({ provider, sdkModels })), + ), ), ), ), @@ -254,22 +258,24 @@ export const CursorDriver: ProviderDriver< Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), Effect.provideService(CursorKeychain.CursorKeychain, keychain), - Effect.map(stampSnapshot), - Effect.tap((provider) => - Effect.gen(function* () { + Effect.map(({ provider, sdkModels }) => ({ + provider: stampSnapshot(provider), + sdkModels, + })), + Effect.tap(({ provider, sdkModels }) => + Effect.sync(() => { // A failed check lists only custom models, so keep the last loaded catalog. if (provider.models.some((model) => !model.isCustom)) { catalogModels = provider.models; } - const apiKey = yield* auth.readApiKey.pipe(Effect.orElseSucceed(() => undefined)); - if (apiKey === undefined || apiKey.trim() === "") return; - const catalog = yield* CursorSdkCatalog.CursorSdkCatalog; - const snapshot = yield* catalog.read(apiKey).pipe(Effect.option); - if (Option.isSome(snapshot)) { - catalogSdkModels = snapshot.value.models; + // Reuse the timed status probe — a second unbounded catalog.read would + // defeat the probe timeout when the first request stalled. + if (sdkModels !== undefined) { + catalogSdkModels = sdkModels; } }), ), + Effect.map(({ provider }) => provider), Effect.provide(CursorSdkCatalog.layer), ); diff --git a/packages/provider-cursor/src/server/status.test.ts b/packages/provider-cursor/src/server/status.test.ts index 6d211d592b500..f04dbdaf6fe68 100644 --- a/packages/provider-cursor/src/server/status.test.ts +++ b/packages/provider-cursor/src/server/status.test.ts @@ -155,7 +155,7 @@ describe("Cursor SDK model discovery", () => { describe("checkCursorProviderStatus", () => { it.effect("uses the SDK catalog when CURSOR_API_KEY is configured", () => Effect.gen(function* () { - const provider = yield* checkCursorProviderStatus( + const { provider, sdkModels } = yield* checkCursorProviderStatus( { ...baseCursorSettings, customModels: ["internal/cursor-model"], @@ -190,12 +190,13 @@ describe("checkCursorProviderStatus", () => { { slug: "internal/cursor-model", isCustom: true }, ], }); + expect(sdkModels).toEqual([sdkParameterizedModel]); }), ); it.effect("surfaces SDK authentication failures", () => Effect.gen(function* () { - const provider = yield* checkCursorProviderStatus(baseCursorSettings, { + const { provider, sdkModels } = yield* checkCursorProviderStatus(baseCursorSettings, { CURSOR_API_KEY: "invalid-test-key", }).pipe( Effect.provide( @@ -215,6 +216,7 @@ describe("checkCursorProviderStatus", () => { auth: { status: "unauthenticated" }, message: "Cursor SDK authentication failed. Check CURSOR_API_KEY.", }); + expect(sdkModels).toBeUndefined(); }), ); @@ -233,7 +235,7 @@ describe("checkCursorProviderStatus", () => { "explains a rejected plan for $authenticationType authentication", ({ authenticationType, message }) => Effect.gen(function* () { - const provider = yield* checkCursorProviderStatus( + const { provider, sdkModels } = yield* checkCursorProviderStatus( baseCursorSettings, { CURSOR_API_KEY: "free-plan-key" }, authenticationType, @@ -258,12 +260,13 @@ describe("checkCursorProviderStatus", () => { auth: { status: "unknown" }, message, }); + expect(sdkModels).toBeUndefined(); }), ); it.effect("requires a Cursor API key without probing any external Cursor binary", () => Effect.gen(function* () { - const provider = yield* checkCursorProviderStatus(baseCursorSettings).pipe( + const { provider, sdkModels } = yield* checkCursorProviderStatus(baseCursorSettings).pipe( Effect.provide( CursorSdkCatalog.layerTest(() => Effect.die("SDK catalog must not be used without CURSOR_API_KEY"), @@ -277,6 +280,7 @@ describe("checkCursorProviderStatus", () => { auth: { status: "unauthenticated" }, message: "Sign in with Cursor or add CURSOR_API_KEY in provider settings.", }); + expect(sdkModels).toBeUndefined(); }), ); }); diff --git a/packages/provider-cursor/src/server/status.ts b/packages/provider-cursor/src/server/status.ts index 9722ddbcfd23a..b94de9276c988 100644 --- a/packages/provider-cursor/src/server/status.ts +++ b/packages/provider-cursor/src/server/status.ts @@ -34,6 +34,11 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ const CURSOR_SDK_CATALOG_TIMEOUT_MS = 15_000; +export interface CursorProviderStatusResult { + readonly provider: ServerProviderDraft; + readonly sdkModels: ReadonlyArray | undefined; +} + export const buildInitialCursorProviderSnapshot = Effect.fn( "CursorProviderStatus.buildInitialCursorProviderSnapshot", )(function* (cursorSettings: CursorSettings): Effect.fn.Return { @@ -248,41 +253,47 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( cursorSettings: CursorSettings, environment?: NodeJS.ProcessEnv, authenticationType: "api-key" | "browser" = "api-key", -): Effect.fn.Return { +): Effect.fn.Return { const checkedAt = DateTime.formatIso(yield* DateTime.now); const fallbackModels = getCursorFallbackModels(cursorSettings); if (!cursorSettings.enabled) { - return buildServerProvider({ - presentation: CURSOR_PRESENTATION, - enabled: false, - checkedAt, - models: fallbackModels, - probe: { - installed: false, - version: null, - status: "warning", - auth: { status: "unknown" }, - message: "Cursor is disabled in T3 Code settings.", - }, - }); + return { + provider: buildServerProvider({ + presentation: CURSOR_PRESENTATION, + enabled: false, + checkedAt, + models: fallbackModels, + probe: { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Cursor is disabled in T3 Code settings.", + }, + }), + sdkModels: undefined, + }; } const sdkApiKey = environment?.CURSOR_API_KEY?.trim(); if (!sdkApiKey) { - return buildServerProvider({ - presentation: CURSOR_PRESENTATION, - enabled: cursorSettings.enabled, - checkedAt, - models: fallbackModels, - probe: { - installed: true, - version: null, - status: "error", - auth: { status: "unauthenticated" }, - message: "Sign in with Cursor or add CURSOR_API_KEY in provider settings.", - }, - }); + return { + provider: buildServerProvider({ + presentation: CURSOR_PRESENTATION, + enabled: cursorSettings.enabled, + checkedAt, + models: fallbackModels, + probe: { + installed: true, + version: null, + status: "error", + auth: { status: "unauthenticated" }, + message: "Sign in with Cursor or add CURSOR_API_KEY in provider settings.", + }, + }), + sdkModels: undefined, + }; } const sdkCatalog = yield* CursorSdkCatalog.CursorSdkCatalog; @@ -307,50 +318,59 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( ? "Cursor needs a paid plan to run in T3 Code. Upgrade this account or sign in with a different one." : "Cursor needs a paid plan to run in T3 Code. Upgrade the account that owns CURSOR_API_KEY or use a key from a paid account."; } - return buildServerProvider({ - presentation: CURSOR_PRESENTATION, - enabled: cursorSettings.enabled, - checkedAt, - models: fallbackModels, - probe: { - installed: true, - version: null, - status: "error", - auth: { status: failure.authenticationFailure ? "unauthenticated" : "unknown" }, - message, - }, - }); + return { + provider: buildServerProvider({ + presentation: CURSOR_PRESENTATION, + enabled: cursorSettings.enabled, + checkedAt, + models: fallbackModels, + probe: { + installed: true, + version: null, + status: "error", + auth: { status: failure.authenticationFailure ? "unauthenticated" : "unknown" }, + message, + }, + }), + sdkModels: undefined, + }; } if (Option.isNone(catalogResult.success)) { - return buildServerProvider({ - presentation: CURSOR_PRESENTATION, - enabled: cursorSettings.enabled, - checkedAt, - models: fallbackModels, - probe: { - installed: true, - version: null, - status: "error", - auth: { status: "unknown" }, - message: `Cursor SDK catalog request timed out after ${CURSOR_SDK_CATALOG_TIMEOUT_MS}ms.`, - }, - }); + return { + provider: buildServerProvider({ + presentation: CURSOR_PRESENTATION, + enabled: cursorSettings.enabled, + checkedAt, + models: fallbackModels, + probe: { + installed: true, + version: null, + status: "error", + auth: { status: "unknown" }, + message: `Cursor SDK catalog request timed out after ${CURSOR_SDK_CATALOG_TIMEOUT_MS}ms.`, + }, + }), + sdkModels: undefined, + }; } const snapshot = catalogResult.success.value; const discoveredModels = buildCursorDiscoveredModelsFromSdk(snapshot.models); - return buildCursorProviderSnapshot({ - checkedAt, - cursorSettings, - parsed: { - version: null, - status: "ready", - auth: cursorSdkAuth(snapshot.user, authenticationType), - }, - discoveredModels, - ...(discoveredModels.length === 0 - ? { discoveryWarning: "Cursor SDK model discovery returned no built-in models." } - : {}), - }); + return { + provider: buildCursorProviderSnapshot({ + checkedAt, + cursorSettings, + parsed: { + version: null, + status: "ready", + auth: cursorSdkAuth(snapshot.user, authenticationType), + }, + discoveredModels, + ...(discoveredModels.length === 0 + ? { discoveryWarning: "Cursor SDK model discovery returned no built-in models." } + : {}), + }), + sdkModels: snapshot.models, + }; }); From b506d51fd0d8985042000156e58e39aa38dde8dd Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Wed, 28 Oct 2026 00:47:32 -0300 Subject: [PATCH 4/8] feat(cursor): expose optional Max Mode as a composer boolean When the SDK catalog has matching max and non-max variants for the same params, add a Max Mode option and honor it on send without forwarding it as a model parameter. Co-authored-by: Cursor --- .../src/server/sdkModel.test.ts | 109 +++++++++++++++++- .../provider-cursor/src/server/sdkModel.ts | 42 ++++++- .../provider-cursor/src/server/status.test.ts | 35 ++++++ packages/provider-cursor/src/server/status.ts | 17 ++- 4 files changed, 197 insertions(+), 6 deletions(-) diff --git a/packages/provider-cursor/src/server/sdkModel.test.ts b/packages/provider-cursor/src/server/sdkModel.test.ts index a19eaa94d2d39..82a3e794898f0 100644 --- a/packages/provider-cursor/src/server/sdkModel.test.ts +++ b/packages/provider-cursor/src/server/sdkModel.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "@effect/vitest"; import { ProviderInstanceId } from "@t3tools/contracts"; -import { cursorSdkMaxModeForParams, cursorSdkModelSelection } from "./sdkModel.ts"; +import { + CURSOR_SDK_MAX_MODE_OPTION_ID, + cursorSdkMaxModeForParams, + cursorSdkModelOffersOptionalMaxMode, + cursorSdkModelSelection, +} from "./sdkModel.ts"; describe("cursorSdkModelSelection", () => { const instanceId = ProviderInstanceId.make("cursor"); @@ -52,4 +57,106 @@ describe("cursorSdkModelSelection", () => { ), ).toBe(false); }); + + it("sets maxMode from the composer option without forwarding it as an SDK param", () => { + const catalog = [ + { + id: "composer-2", + variants: [ + { + displayName: "Composer", + isDefault: true, + params: [{ id: "fast", value: "false" }], + }, + { + displayName: "Composer Max", + isMaxMode: true, + params: [{ id: "fast", value: "false" }], + }, + ], + }, + ]; + expect( + cursorSdkModelSelection( + { + instanceId, + model: "composer-2", + options: [ + { id: "fastMode", value: false }, + { id: CURSOR_SDK_MAX_MODE_OPTION_ID, value: true }, + ], + }, + catalog, + ), + ).toEqual({ + id: "composer-2", + params: [{ id: "fast", value: "false" }], + maxMode: true, + }); + expect( + cursorSdkModelSelection( + { + instanceId, + model: "composer-2", + options: [ + { id: "fastMode", value: false }, + { id: CURSOR_SDK_MAX_MODE_OPTION_ID, value: false }, + ], + }, + catalog, + ), + ).toEqual({ + id: "composer-2", + params: [{ id: "fast", value: "false" }], + }); + }); +}); + +describe("cursorSdkModelOffersOptionalMaxMode", () => { + it("is true when the same params have both max and non-max variants", () => { + expect( + cursorSdkModelOffersOptionalMaxMode({ + variants: [ + { + displayName: "Standard", + isDefault: true, + params: [{ id: "fast", value: "false" }], + }, + { + displayName: "Max", + isMaxMode: true, + params: [{ id: "fast", value: "false" }], + }, + ], + }), + ).toBe(true); + }); + + it("is false when max mode is forced for every matching selection", () => { + expect( + cursorSdkModelOffersOptionalMaxMode({ + variants: [ + { + displayName: "1M", + isMaxMode: true, + params: [{ id: "context", value: "1m" }], + }, + ], + }), + ).toBe(false); + }); + + it("is false when no variants use max mode", () => { + expect( + cursorSdkModelOffersOptionalMaxMode({ + variants: [ + { + displayName: "Default", + isDefault: true, + params: [{ id: "fast", value: "false" }], + }, + ], + }), + ).toBe(false); + }); }); diff --git a/packages/provider-cursor/src/server/sdkModel.ts b/packages/provider-cursor/src/server/sdkModel.ts index a3125ed7628bb..000de871e4586 100644 --- a/packages/provider-cursor/src/server/sdkModel.ts +++ b/packages/provider-cursor/src/server/sdkModel.ts @@ -7,6 +7,9 @@ import type { import type { ModelSelection, ServerProviderModel } from "@t3tools/contracts"; import { getProviderOptionCurrentValue } from "@t3tools/shared/model"; +/** Composer option id for optional Cursor max mode; not an SDK model parameter. */ +export const CURSOR_SDK_MAX_MODE_OPTION_ID = "maxMode"; + const CURSOR_SDK_PARAMETER_TO_PROVIDER_OPTION: Readonly> = { context: "contextWindow", fast: "fastMode", @@ -72,7 +75,6 @@ function variantMatchesParams( ); } -/** True when every catalog variant consistent with `params` requires max mode. */ export function cursorSdkMaxModeForParams( model: Pick, params: ReadonlyArray | undefined, @@ -82,21 +84,53 @@ export function cursorSdkMaxModeForParams( return matching.length > 0 && matching.every((variant) => variant.isMaxMode === true); } +export function cursorSdkModelOffersOptionalMaxMode(model: Pick): boolean { + const variants = model.variants ?? []; + if ( + !variants.some((variant) => variant.isMaxMode === true) || + !variants.some((variant) => variant.isMaxMode !== true) + ) { + return false; + } + + const candidateParams: Array | undefined> = [undefined]; + for (const variant of variants) { + candidateParams.push(variant.params); + } + + for (const params of candidateParams) { + const matching = variants.filter((variant) => variantMatchesParams(variant, params)); + if ( + matching.some((variant) => variant.isMaxMode === true) && + matching.some((variant) => variant.isMaxMode !== true) + ) { + return true; + } + } + return false; +} + export function cursorSdkModelSelection( modelSelection: ModelSelection, sdkModels?: ReadonlyArray>, ): CursorSdkModelSelection { const id = modelSelection.model === "auto" ? "default" : modelSelection.model; + const options = modelSelection.options ?? []; + const userRequestedMaxMode = options.some( + (option) => option.id === CURSOR_SDK_MAX_MODE_OPTION_ID && option.value === true, + ); + const sdkOptions = options.filter((option) => option.id !== CURSOR_SDK_MAX_MODE_OPTION_ID); const params = - modelSelection.options === undefined || modelSelection.options.length === 0 + sdkOptions.length === 0 ? undefined - : modelSelection.options.map((option): ModelParameterValue => ({ + : sdkOptions.map((option): ModelParameterValue => ({ id: cursorSdkParameterId(option.id), value: String(option.value), })); const catalogModel = sdkModels?.find((model) => model.id === id); const maxMode = - catalogModel === undefined ? false : cursorSdkMaxModeForParams(catalogModel, params); + userRequestedMaxMode || + (catalogModel !== undefined && cursorSdkMaxModeForParams(catalogModel, params)); return { id, ...(params === undefined ? {} : { params }), diff --git a/packages/provider-cursor/src/server/status.test.ts b/packages/provider-cursor/src/server/status.test.ts index f04dbdaf6fe68..9b8331dbc8e2f 100644 --- a/packages/provider-cursor/src/server/status.test.ts +++ b/packages/provider-cursor/src/server/status.test.ts @@ -92,6 +92,30 @@ const sdkParameterizedModel = { ], } satisfies SDKModel; +const sdkOptionalMaxModeModel = { + id: "composer-2", + displayName: "Composer 2", + parameters: [ + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + displayName: "Composer 2", + isDefault: true, + params: [{ id: "fast", value: "false" }], + }, + { + displayName: "Composer 2 Max", + isMaxMode: true, + params: [{ id: "fast", value: "false" }], + }, + ], +} satisfies SDKModel; + describe("buildCursorProviderSnapshot", () => { it("downgrades ready status to warning when SDK model discovery returns no models", () => { expect( @@ -134,6 +158,17 @@ describe("Cursor SDK model discovery", () => { ); }); + it("adds a Max Mode boolean when the catalog offers optional max mode", () => { + expect(buildCursorCapabilitiesFromSdkModel(sdkOptionalMaxModeModel)).toEqual( + createModelCapabilities({ + optionDescriptors: [ + booleanDescriptor("fastMode", "Fast", false), + booleanDescriptor("maxMode", "Max Mode", false), + ], + }), + ); + }); + it("filters invalid and duplicate SDK model entries", () => { expect( buildCursorDiscoveredModelsFromSdk([ diff --git a/packages/provider-cursor/src/server/status.ts b/packages/provider-cursor/src/server/status.ts index b94de9276c988..af3539d42336d 100644 --- a/packages/provider-cursor/src/server/status.ts +++ b/packages/provider-cursor/src/server/status.ts @@ -13,7 +13,12 @@ import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Result from "effect/Result"; -import { cursorSdkParameterPriority, cursorSdkProviderOptionId } from "./sdkModel.ts"; +import { + CURSOR_SDK_MAX_MODE_OPTION_ID, + cursorSdkModelOffersOptionalMaxMode, + cursorSdkParameterPriority, + cursorSdkProviderOptionId, +} from "./sdkModel.ts"; import { buildBooleanOptionDescriptor, buildSelectOptionDescriptor, @@ -162,6 +167,16 @@ export function buildCursorCapabilitiesFromSdkModel(model: SDKModel): ModelCapab ); } + if (cursorSdkModelOffersOptionalMaxMode(model)) { + optionDescriptors.push( + buildBooleanOptionDescriptor({ + id: CURSOR_SDK_MAX_MODE_OPTION_ID, + label: "Max Mode", + currentValue: false, + }), + ); + } + return createModelCapabilities({ optionDescriptors }); } From 2942ffac5f8db408b0eb8f945e4f88798ed48327 Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Wed, 28 Oct 2026 14:38:30 -0300 Subject: [PATCH 5/8] fix(cursor): ignore Max Mode when params have no max variant Composer can leave Max Mode on after switching to a param set that only has non-max catalog variants; require a matching isMaxMode variant before honoring the option. Co-authored-by: Cursor --- .../src/server/sdkModel.test.ts | 20 +++++++++++++++++++ .../provider-cursor/src/server/sdkModel.ts | 18 ++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/provider-cursor/src/server/sdkModel.test.ts b/packages/provider-cursor/src/server/sdkModel.test.ts index 82a3e794898f0..d90ea1ac77c6c 100644 --- a/packages/provider-cursor/src/server/sdkModel.test.ts +++ b/packages/provider-cursor/src/server/sdkModel.test.ts @@ -73,6 +73,10 @@ describe("cursorSdkModelSelection", () => { isMaxMode: true, params: [{ id: "fast", value: "false" }], }, + { + displayName: "Composer Fast", + params: [{ id: "fast", value: "true" }], + }, ], }, ]; @@ -109,6 +113,22 @@ describe("cursorSdkModelSelection", () => { id: "composer-2", params: [{ id: "fast", value: "false" }], }); + expect( + cursorSdkModelSelection( + { + instanceId, + model: "composer-2", + options: [ + { id: "fastMode", value: true }, + { id: CURSOR_SDK_MAX_MODE_OPTION_ID, value: true }, + ], + }, + catalog, + ), + ).toEqual({ + id: "composer-2", + params: [{ id: "fast", value: "true" }], + }); }); }); diff --git a/packages/provider-cursor/src/server/sdkModel.ts b/packages/provider-cursor/src/server/sdkModel.ts index 000de871e4586..8ddcc52cd67b5 100644 --- a/packages/provider-cursor/src/server/sdkModel.ts +++ b/packages/provider-cursor/src/server/sdkModel.ts @@ -84,6 +84,15 @@ export function cursorSdkMaxModeForParams( return matching.length > 0 && matching.every((variant) => variant.isMaxMode === true); } +function cursorSdkParamsAdmitMaxMode( + model: Pick, + params: ReadonlyArray | undefined, +): boolean { + return (model.variants ?? []).some( + (variant) => variantMatchesParams(variant, params) && variant.isMaxMode === true, + ); +} + export function cursorSdkModelOffersOptionalMaxMode(model: Pick): boolean { const variants = model.variants ?? []; if ( @@ -128,9 +137,12 @@ export function cursorSdkModelSelection( value: String(option.value), })); const catalogModel = sdkModels?.find((model) => model.id === id); - const maxMode = - userRequestedMaxMode || - (catalogModel !== undefined && cursorSdkMaxModeForParams(catalogModel, params)); + const forcedMaxMode = + catalogModel !== undefined && cursorSdkMaxModeForParams(catalogModel, params); + const optionalMaxMode = + userRequestedMaxMode && + (catalogModel === undefined || cursorSdkParamsAdmitMaxMode(catalogModel, params)); + const maxMode = forcedMaxMode || optionalMaxMode; return { id, ...(params === undefined ? {} : { params }), From 3c0bc2c708db1af22f5041f21b7dbde22b6aaff6 Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Wed, 28 Oct 2026 14:43:36 -0300 Subject: [PATCH 6/8] docs(cursor): drop SDK API names from the user guide Describe ambient MCP/plugins and compaction/snapshots in product terms instead of settingSources and agent APIs. Co-authored-by: Cursor --- docs/user/cursor.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/cursor.md b/docs/user/cursor.md index 3882fe0834b86..b93ae675e66dc 100644 --- a/docs/user/cursor.md +++ b/docs/user/cursor.md @@ -17,7 +17,7 @@ not expose yet. Custom Cursor MCP servers and plugins are not configured in T3 Code. They load from the environment machine's Cursor config when the agent opens: project `.cursor/mcp.json`, user `~/.cursor/mcp.json`, -and installed plugins under `~/.cursor/plugins`, via the SDK `settingSources` layers. Plugin tools +and installed plugins under `~/.cursor/plugins`. Plugin tools can appear without a login; OAuth MCP servers still need credentials. Run `agent mcp login ` with the cwd set to the T3 project root (tokens land in `~/.cursor/projects//mcp-auth.json`). Cursor desktop Connections auth does not write @@ -50,8 +50,8 @@ The adapter supports: - thread-scoped T3 Code MCP tools alongside ambient Cursor MCP/plugins from disk; - image attachments; - interruption, queued app messages, and orchestrator-owned interrupt/restart steering; -- native conversation compaction through `agent.summarize()`; -- provider conversation snapshots through `Agent.messages.list()`; +- native conversation compaction; +- provider conversation snapshots; - Cursor `task` subagents, projected as read-only child app threads with their tool activity and final result. From 528aaeb2eabdc487c6dbd5fe2b996a7c1fefb8cc Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Fri, 30 Oct 2026 15:03:52 -0300 Subject: [PATCH 7/8] chore(cursor): retrigger CI after cancelled workflow run Co-authored-by: Cursor From 45045b5826f7045f7a3975b746326165c105b3c6 Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Date: Fri, 30 Oct 2026 15:18:41 -0300 Subject: [PATCH 8/8] fix(cursor): teach main's session mocks summarize and reset told on compact CI merges with main, which added device/task-ls Cursor session mocks without summarize. Reset instruction delivery after native summarize so the next turn reinjects orchestration and device prompts. Co-authored-by: Cursor --- .../src/server/adapter.test.ts | 28 +++++++++++++++++++ .../provider-cursor/src/server/adapter.ts | 5 ++++ 2 files changed, 33 insertions(+) diff --git a/packages/provider-cursor/src/server/adapter.test.ts b/packages/provider-cursor/src/server/adapter.test.ts index a83ff8dcf5ca0..b824002a952b0 100644 --- a/packages/provider-cursor/src/server/adapter.test.ts +++ b/packages/provider-cursor/src/server/adapter.test.ts @@ -686,6 +686,20 @@ describe("CursorAdapterV2", () => { agentId: "native-cursor-device", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => + Effect.succeed({ + agentId: "native-cursor-device", + runId: "native-cursor-summarize", + wait: Effect.succeed({ + id: "native-cursor-summarize", + requestId: "native-summarize-request", + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + result: "summary", + }), + cancel: Effect.void, + }), send: (input) => Effect.sync(() => { sentMessages.push( @@ -1293,6 +1307,20 @@ describe("CursorAdapterV2", () => { agentId: "native-cursor-task-ls", listMessages: Effect.succeed([]), close: Effect.void, + summarize: () => + Effect.succeed({ + agentId: "native-cursor-task-ls", + runId: "native-cursor-summarize", + wait: Effect.succeed({ + id: "native-cursor-summarize", + requestId: "native-summarize-request", + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + result: "summary", + }), + cancel: Effect.void, + }), send: (input) => Effect.gen(function* () { const args = { diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index 48cb4872fbbee..715ff87083a54 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -2800,6 +2800,11 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( modelSelection: turnInput.modelSelection, runtimePolicy: turnInput.runtimePolicy, }); + yield* Ref.set(told, { + agent: agent.nativeThreadId, + orchestration: false, + device: false, + }); const pendingUpdates: Array = []; let context: ActiveCursorTurn | null = null; const sdkRun = yield* agent.session