pi-cursor-sdk 0.1.56 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +100 -28
  3. package/docs/cursor-live-smoke-checklist.md +7 -7
  4. package/docs/cursor-model-ux-spec.md +45 -38
  5. package/docs/cursor-native-tool-replay.md +2 -2
  6. package/docs/cursor-native-tool-visual-audit.md +1 -1
  7. package/docs/cursor-testing-lessons.md +20 -7
  8. package/docs/cursor-tool-surfaces.md +13 -2
  9. package/docs/platform-smoke-implementation.md +220 -0
  10. package/docs/platform-smoke.md +183 -247
  11. package/package.json +38 -6
  12. package/platform-smoke.config.mjs +5 -1
  13. package/scripts/cloud-runtime-smoke.d.mts +3 -0
  14. package/scripts/cloud-runtime-smoke.mjs +502 -0
  15. package/scripts/debug-provider-events.mjs +7 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -6
  17. package/scripts/lib/cursor-child-process.d.mts +1 -0
  18. package/scripts/lib/cursor-child-process.mjs +137 -7
  19. package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
  20. package/scripts/local-resume-cleanup-smoke.mjs +108 -0
  21. package/scripts/local-resume-smoke.d.mts +1 -0
  22. package/scripts/local-resume-smoke.mjs +642 -0
  23. package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
  24. package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
  25. package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
  26. package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
  27. package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
  28. package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
  29. package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
  30. package/scripts/platform-smoke/artifacts.mjs +293 -65
  31. package/scripts/platform-smoke/card-detect.mjs +16 -4
  32. package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
  33. package/scripts/platform-smoke/doctor.mjs +20 -10
  34. package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
  35. package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
  36. package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
  37. package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
  38. package/scripts/platform-smoke/scenarios.mjs +16 -2
  39. package/scripts/platform-smoke/target-runtime.mjs +206 -0
  40. package/scripts/platform-smoke/targets.mjs +33 -141
  41. package/scripts/platform-smoke/visual-evidence.mjs +6 -7
  42. package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
  43. package/scripts/platform-smoke.mjs +40 -27
  44. package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
  45. package/scripts/steering-rpc-smoke.mjs +12 -2
  46. package/scripts/tmux-live-smoke.sh +3 -5
  47. package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
  48. package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
  49. package/shared/cursor-sensitive-text.mjs +7 -1
  50. package/src/context.ts +5 -2
  51. package/src/cursor-agents-context-registration.ts +7 -0
  52. package/src/cursor-agents-context.ts +3 -1
  53. package/src/cursor-api-key.ts +15 -1
  54. package/src/cursor-bridge-contract.ts +3 -0
  55. package/src/cursor-cloud-lifecycle.ts +733 -0
  56. package/src/cursor-cloud-options.ts +206 -0
  57. package/src/cursor-cloud-reporting.ts +246 -0
  58. package/src/cursor-config.ts +659 -0
  59. package/src/cursor-display-only-trace.ts +14 -0
  60. package/src/cursor-display-text.ts +8 -2
  61. package/src/cursor-durable-fs.ts +49 -0
  62. package/src/cursor-fallback-models.generated.ts +2045 -485
  63. package/src/cursor-live-run-accounting.ts +7 -1
  64. package/src/cursor-live-run-coordinator.ts +1 -0
  65. package/src/cursor-pi-tool-bridge-run.ts +14 -4
  66. package/src/cursor-provider-live-run-drain.ts +5 -0
  67. package/src/cursor-provider-run-finalizer.ts +36 -28
  68. package/src/cursor-provider-turn-finalize.ts +72 -6
  69. package/src/cursor-provider-turn-prepare.ts +228 -12
  70. package/src/cursor-provider-turn-runner.ts +41 -13
  71. package/src/cursor-provider-turn-send.ts +59 -16
  72. package/src/cursor-provider-turn-types.ts +44 -10
  73. package/src/cursor-runtime-state.ts +478 -0
  74. package/src/cursor-sdk-event-debug.ts +46 -6
  75. package/src/cursor-sdk-process-error-guard.ts +101 -30
  76. package/src/cursor-session-agent-cleanup.ts +328 -0
  77. package/src/cursor-session-agent-resume.ts +439 -0
  78. package/src/cursor-session-agent.ts +109 -13
  79. package/src/cursor-session-scope.ts +35 -2
  80. package/src/cursor-session-send-policy.ts +1 -1
  81. package/src/cursor-skill-tool.ts +30 -11
  82. package/src/cursor-state.ts +112 -69
  83. package/src/cursor-usage-accounting.ts +14 -4
  84. package/src/index.ts +11 -2
  85. package/src/model-discovery.ts +10 -56
@@ -1,4 +1,5 @@
1
- import type { SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
1
+ import type { Context, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
2
+ import type { AgentModeOption, ModelSelection, SDKAgent } from "@cursor/sdk";
2
3
  import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
3
4
  import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
4
5
  import {
@@ -6,9 +7,10 @@ import {
6
7
  buildCursorSessionSendPrompt,
7
8
  planCursorSessionSend,
8
9
  resetSessionCursorAgent,
10
+ type CursorSessionSendPlan,
9
11
  } from "./cursor-session-agent.js";
10
12
  import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
11
- import { estimateCursorPromptTokens } from "./context.js";
13
+ import { buildCursorPrompt, estimateCursorPromptTokens } from "./context.js";
12
14
  import { getCursorPromptOptions } from "./cursor-usage-accounting.js";
13
15
  import { getActiveContextToolNames } from "./cursor-context-tools.js";
14
16
  import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
@@ -16,25 +18,47 @@ import {
16
18
  abandonSessionCursorAgent,
17
19
  createCursorNativeReplayId,
18
20
  cursorLiveRuns,
21
+ getActiveCursorLiveRunForCurrentScope,
22
+ getPendingCursorLiveRun,
19
23
  } from "./cursor-provider-live-run-drain.js";
20
- import { getCursorProviderAgentModeOrThrow, getEffectiveFastForModelId } from "./cursor-state.js";
24
+ import {
25
+ getCursorProviderAgentModeOrThrow,
26
+ getEffectiveFastForModelId,
27
+ } from "./cursor-state.js";
28
+ import { resolveEffectiveCursorConfig } from "./cursor-runtime-state.js";
29
+ import type { CursorResolvedSdkConfig } from "./cursor-config.js";
21
30
  import { buildCursorModelSelection } from "./model-discovery.js";
22
31
  import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
32
+ import {
33
+ formatCursorCloudPreflightError,
34
+ buildCursorCloudAgentOptions,
35
+ inspectCursorCloudLocalState,
36
+ preflightCursorCloudRuntime,
37
+ } from "./cursor-cloud-options.js";
38
+ import { getCursorSessionName, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
23
39
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
24
40
  import {
25
41
  buildCursorToolManifestText,
26
42
  resolveCursorToolManifestEnabled,
27
43
  } from "./cursor-tool-manifest.js";
28
44
  import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display-state.js";
45
+ import {
46
+ createCursorCloudLifecyclePersistenceError,
47
+ recordCursorCloudLifecycleSafely,
48
+ } from "./cursor-cloud-lifecycle.js";
29
49
  import { MISSING_CURSOR_API_KEY_MESSAGE } from "./cursor-provider-errors.js";
30
50
  import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
31
51
  import { resolveCursorApiKey } from "./cursor-api-key.js";
32
52
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
33
53
  import type {
54
+ CloudCursorProviderTurnPrepareResult,
55
+ CursorProviderTurnLifecycle,
34
56
  CursorProviderTurnPrepareResult,
35
57
  CursorProviderTurnRunnerParams,
58
+ LocalCursorProviderTurnPrepareResult,
36
59
  } from "./cursor-provider-turn-types.js";
37
60
  import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
61
+ import type { SessionCursorAgentLease } from "./cursor-session-agent.js";
38
62
 
39
63
  export interface PrepareCursorProviderTurnParams {
40
64
  params: CursorProviderTurnRunnerParams;
@@ -42,12 +66,173 @@ export interface PrepareCursorProviderTurnParams {
42
66
  resolvedApiKey: string;
43
67
  sdkEventDebug: CursorSdkEventDebugSink | undefined;
44
68
  throwIfAborted: () => void;
69
+ /** Snapshot resolved once by the runner before draining; reused unchanged through prepare. */
70
+ resolvedConfig: CursorResolvedSdkConfig;
45
71
  }
46
72
 
47
- export async function prepareCursorProviderTurn(
48
- prepareParams: PrepareCursorProviderTurnParams,
49
- ): Promise<CursorProviderTurnPrepareResult> {
50
- const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted } = prepareParams;
73
+ interface PrepareCursorProviderTurnContext extends PrepareCursorProviderTurnParams {
74
+ agentMode: AgentModeOption;
75
+ selection: ModelSelection;
76
+ fastEnabled: boolean | undefined;
77
+ }
78
+
79
+ function buildCursorCloudPromptContext(context: Context, handoff: "fresh" | "bootstrap" | "never"): Context {
80
+ if (handoff === "bootstrap") return context;
81
+ for (let index = context.messages.length - 1; index >= 0; index -= 1) {
82
+ const message = context.messages[index];
83
+ if (message.role === "user") return { ...context, messages: [message] };
84
+ }
85
+ return { ...context, messages: context.messages.slice(-1) };
86
+ }
87
+
88
+ const CLOUD_SEND_PLAN: CursorSessionSendPlan = { mode: "bootstrap", resetAgent: false, reason: "initial" };
89
+
90
+ export function resolveCursorProviderTurnConfig(cwd: string) {
91
+ return resolveEffectiveCursorConfig({ cwd, projectTrusted: getCursorSessionProjectTrusted() });
92
+ }
93
+
94
+ function buildCloudCursorProviderTurnLifecycle(agent: SDKAgent): CursorProviderTurnLifecycle {
95
+ return {
96
+ trackRunCompletion: () => {},
97
+ commitSend: () => {},
98
+ abandon: async () => {},
99
+ dispose: async () => {
100
+ await agent[Symbol.asyncDispose]?.();
101
+ },
102
+ };
103
+ }
104
+
105
+ function buildLocalCursorProviderTurnLifecycle(
106
+ lease: SessionCursorAgentLease,
107
+ scopeKey: string,
108
+ ): CursorProviderTurnLifecycle {
109
+ return {
110
+ trackRunCompletion: (completion) => lease.trackRunCompletion(completion),
111
+ commitSend: (context, bootstrapped) => lease.commitSend(context, bootstrapped),
112
+ abandon: () => abandonSessionCursorAgent(scopeKey),
113
+ dispose: async () => {},
114
+ };
115
+ }
116
+
117
+ async function prepareCursorCloudProviderTurn(
118
+ prepareParams: PrepareCursorProviderTurnContext,
119
+ ): Promise<CloudCursorProviderTurnPrepareResult> {
120
+ const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled } = prepareParams;
121
+ const { model, context, options } = params;
122
+
123
+ let restoreCursorSdkOutputFilter: (() => void) | undefined;
124
+ let cloudAgentForCleanup: SDKAgent | undefined;
125
+ let completed = false;
126
+
127
+ try {
128
+ const preflight = preflightCursorCloudRuntime({
129
+ resolvedConfig,
130
+ localState: inspectCursorCloudLocalState(cwd),
131
+ hasPriorContext: context.messages.length > 1,
132
+ });
133
+ if (!preflight.ok) throw new Error(formatCursorCloudPreflightError(preflight));
134
+ if (getPendingCursorLiveRun(context) || getActiveCursorLiveRunForCurrentScope()) {
135
+ throw new Error("Cursor cloud runtime cannot start while a local Cursor live run is pending; finish or abort the local run, then retry.");
136
+ }
137
+
138
+ const { Agent } = await loadCursorSdk();
139
+ restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
140
+ const promptOptions = {
141
+ ...getCursorPromptOptions(model),
142
+ agentMode,
143
+ includePiBridgeGuidance: false,
144
+ includePiAskQuestionGuidance: false,
145
+ };
146
+ const prompt = buildCursorPrompt(
147
+ buildCursorCloudPromptContext(context, resolvedConfig.cloud.contextHandoff.value),
148
+ promptOptions,
149
+ );
150
+ const promptInputTokens = estimateCursorPromptTokens(prompt, promptOptions);
151
+ const agent = await suppressCursorSdkOutput(() =>
152
+ Agent.create(buildCursorCloudAgentOptions({
153
+ apiKey: resolvedApiKey,
154
+ modelSelection: selection,
155
+ agentMode,
156
+ resolvedConfig,
157
+ name: getCursorSessionName(),
158
+ })),
159
+ );
160
+ cloudAgentForCleanup = agent;
161
+ if (!recordCursorCloudLifecycleSafely({ agentId: agent.agentId }, resolvedApiKey)) {
162
+ throw createCursorCloudLifecyclePersistenceError(agent.agentId, "intent", undefined, resolvedApiKey);
163
+ }
164
+ sdkEventDebug?.recordProviderMeta({ runtime: "cloud", cloudAgentId: agent.agentId, phase: "agent_created" });
165
+ throwIfAborted();
166
+
167
+ const textDeltas: string[] = [];
168
+ const nativeReplayId = createCursorNativeReplayId();
169
+ const turnCoordinator = new CursorSdkTurnCoordinator({
170
+ stream: params.stream,
171
+ partial: params.partial,
172
+ cwd,
173
+ resolvedApiKey,
174
+ useNativeToolReplay: false,
175
+ nativeReplayId,
176
+ textDeltas,
177
+ debugRecorder: sdkEventDebug,
178
+ });
179
+ sdkEventDebug?.recordProviderMeta({
180
+ runtime: "cloud",
181
+ cloudAgentId: agent.agentId,
182
+ model: {
183
+ id: model.id,
184
+ provider: model.provider,
185
+ api: model.api,
186
+ reasoning: options?.reasoning ?? "off",
187
+ fastEnabled,
188
+ selection,
189
+ },
190
+ contextHandoff: resolvedConfig.cloud.contextHandoff.value,
191
+ sendPlan: CLOUD_SEND_PLAN,
192
+ promptOptions,
193
+ agentMode,
194
+ localForce: false,
195
+ });
196
+
197
+ completed = true;
198
+ cloudAgentForCleanup = undefined;
199
+ return {
200
+ runtimeTarget: "cloud",
201
+ agent,
202
+ cwd,
203
+ payload: {
204
+ text: prompt.text,
205
+ images: prompt.images.length > 0 ? prompt.images : undefined,
206
+ },
207
+ meta: {
208
+ sendPlan: CLOUD_SEND_PLAN,
209
+ prompt,
210
+ bootstrap: true,
211
+ promptInputTokens,
212
+ useNativeToolReplay: false,
213
+ bridgeEnabled: false,
214
+ nativeReplayId,
215
+ agentMode,
216
+ modelSelection: selection,
217
+ },
218
+ contextWindowAgentId: agent.agentId,
219
+ textDeltas,
220
+ restoreCursorSdkOutputFilter,
221
+ lifecycle: buildCloudCursorProviderTurnLifecycle(agent),
222
+ runtime: { kind: "direct", turnCoordinator },
223
+ };
224
+ } finally {
225
+ if (!completed) {
226
+ await cloudAgentForCleanup?.[Symbol.asyncDispose]?.().catch(() => {});
227
+ restoreCursorSdkOutputFilter?.();
228
+ }
229
+ }
230
+ }
231
+
232
+ async function prepareCursorLocalProviderTurn(
233
+ prepareParams: PrepareCursorProviderTurnContext,
234
+ ): Promise<LocalCursorProviderTurnPrepareResult> {
235
+ const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled } = prepareParams;
51
236
  const { model, context, options } = params;
52
237
 
53
238
  let restoreCursorSdkOutputFilter: (() => void) | undefined;
@@ -56,14 +241,15 @@ export async function prepareCursorProviderTurn(
56
241
  let completed = false;
57
242
 
58
243
  try {
59
- const fastEnabled = getEffectiveFastForModelId(model.id);
60
- const agentMode = getCursorProviderAgentModeOrThrow();
61
- const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
62
- const settingSources = getEffectiveCursorSettingSources();
244
+ const localSafety = {
245
+ autoReview: resolvedConfig.local.autoReview.value,
246
+ sandboxEnabled: resolvedConfig.local.sandboxEnabled.value,
247
+ };
63
248
  const { Agent } = await loadCursorSdk();
64
249
 
65
250
  installCursorMcpToolTimeoutOverride();
66
251
  restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
252
+ const settingSources = getEffectiveCursorSettingSources();
67
253
  const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
68
254
  let liveRunForBridgeQueue: CursorLiveRun | undefined;
69
255
 
@@ -73,6 +259,8 @@ export async function prepareCursorProviderTurn(
73
259
  cwd,
74
260
  modelSelection: selection,
75
261
  settingSources,
262
+ localSafety,
263
+ localResume: resolvedConfig.local.resume.value,
76
264
  debugRecorder: sdkEventDebug,
77
265
  onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
78
266
  if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
@@ -110,11 +298,14 @@ export async function prepareCursorProviderTurn(
110
298
  };
111
299
  };
112
300
  let sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
301
+ if (sessionAgentLease.created && sessionAgentLease.resumed && sendPlan.mode === "incremental") {
302
+ sendPlan = { mode: "bootstrap", resetAgent: false, reason: "process_resume" };
303
+ }
113
304
  let promptOptions = buildPromptOptions(sendPlan);
114
305
  let prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
115
306
  if (sendPlan.resetAgent) {
116
307
  await resetSessionCursorAgent(sessionAgentScopeKey);
117
- sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
308
+ sessionAgentLease = await acquireSessionCursorAgent({ ...sessionAgentAcquireParams, forceCreate: true });
118
309
  sessionAgentScopeKey = sessionAgentLease.scopeKey;
119
310
  bridgeToolNames = new Set(sessionAgentLease.bridgeRun?.snapshot.tools.map((tool) => tool.mcpToolName) ?? []);
120
311
  includePiBridgeGuidance = bridgeToolNames.size > 0;
@@ -148,6 +339,9 @@ export async function prepareCursorProviderTurn(
148
339
  promptOptions,
149
340
  toolManifestEnabled: resolveCursorToolManifestEnabled(),
150
341
  agentMode,
342
+ localForce: resolvedConfig.local.force.value,
343
+ localResume: resolvedConfig.local.resume.value,
344
+ resumedAgent: sessionAgentLease.resumed,
151
345
  activeToolNames: activeToolNames ? [...activeToolNames] : [],
152
346
  sessionAgentScopeKey,
153
347
  bridgeRunId: bridgeRun?.id,
@@ -188,6 +382,7 @@ export async function prepareCursorProviderTurn(
188
382
 
189
383
  completed = true;
190
384
  return {
385
+ runtimeTarget: "local",
191
386
  agent,
192
387
  cwd,
193
388
  payload: sendPayload,
@@ -200,12 +395,16 @@ export async function prepareCursorProviderTurn(
200
395
  bridgeEnabled: bridgeRun !== undefined,
201
396
  nativeReplayId,
202
397
  agentMode,
398
+ modelSelection: selection,
399
+ ...(sessionAgentLease.resumeNotice ? { resumeNotice: sessionAgentLease.resumeNotice } : {}),
203
400
  },
204
401
  contextWindowAgentId: agent.agentId,
205
402
  textDeltas,
206
403
  sessionAgentScopeKey,
207
404
  sessionAgentLease,
405
+ localForce: resolvedConfig.local.force,
208
406
  restoreCursorSdkOutputFilter,
407
+ lifecycle: buildLocalCursorProviderTurnLifecycle(sessionAgentLease, sessionAgentScopeKey),
209
408
  runtime: liveRun
210
409
  ? { kind: "live", liveRun, turnCoordinator }
211
410
  : { kind: "direct", turnCoordinator },
@@ -224,6 +423,23 @@ export async function prepareCursorProviderTurn(
224
423
  }
225
424
  }
226
425
 
426
+ /** Resolves the runtime target from the caller-supplied snapshot and dispatches to the owning branch. */
427
+ export async function prepareCursorProviderTurn(
428
+ prepareParams: PrepareCursorProviderTurnParams,
429
+ ): Promise<CursorProviderTurnPrepareResult> {
430
+ const { params, resolvedConfig } = prepareParams;
431
+ const { model, options } = params;
432
+
433
+ const agentMode = getCursorProviderAgentModeOrThrow();
434
+ const fastEnabled = resolvedConfig.runtime.value === "cloud" ? undefined : getEffectiveFastForModelId(model.id);
435
+ const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
436
+ const context: PrepareCursorProviderTurnContext = { ...prepareParams, agentMode, selection, fastEnabled };
437
+
438
+ return resolvedConfig.runtime.value === "cloud"
439
+ ? prepareCursorCloudProviderTurn(context)
440
+ : prepareCursorLocalProviderTurn(context);
441
+ }
442
+
227
443
  export function requireCursorApiKey(options: SimpleStreamOptions | undefined): string {
228
444
  const apiKey = resolveCursorApiKey(options?.apiKey);
229
445
  if (!apiKey) throw new Error(MISSING_CURSOR_API_KEY_MESSAGE);
@@ -9,16 +9,31 @@ import {
9
9
  emitCursorLiveTurn,
10
10
  } from "./cursor-provider-turn-emit.js";
11
11
  import { CursorRunFinalizer, type CursorLiveRunCompletion } from "./cursor-provider-run-finalizer.js";
12
- import { prepareCursorProviderTurn, requireCursorApiKey } from "./cursor-provider-turn-prepare.js";
12
+ import {
13
+ prepareCursorProviderTurn,
14
+ requireCursorApiKey,
15
+ resolveCursorProviderTurnConfig,
16
+ } from "./cursor-provider-turn-prepare.js";
13
17
  import { sendCursorProviderTurn } from "./cursor-provider-turn-send.js";
14
18
  import type {
15
19
  CursorProviderTurnPrepareResult,
16
20
  CursorProviderTurnRunnerParams,
17
21
  CursorProviderTurnSendResult,
22
+ LiveCursorProviderTurnRuntime,
23
+ LocalCursorProviderTurnPrepareResult,
18
24
  } from "./cursor-provider-turn-types.js";
19
25
 
20
26
  export type { CursorProviderTurnRunnerParams } from "./cursor-provider-turn-types.js";
21
27
 
28
+ type LocalLivePreparedTurn = LocalCursorProviderTurnPrepareResult & { runtime: LiveCursorProviderTurnRuntime };
29
+
30
+ function requireLocalLivePreparedTurn(prepared: CursorProviderTurnPrepareResult): LocalLivePreparedTurn {
31
+ if (prepared.runtimeTarget !== "local" || prepared.runtime.kind !== "live") {
32
+ throw new Error("Cursor live run requires a local live prepared turn");
33
+ }
34
+ return prepared as LocalLivePreparedTurn;
35
+ }
36
+
22
37
  export class CursorProviderTurnRunner {
23
38
  private sdkEventDebug: CursorSdkEventDebugSink | undefined;
24
39
  private resolvedApiKey: string | undefined;
@@ -55,11 +70,16 @@ export class CursorProviderTurnRunner {
55
70
  });
56
71
  sdkEventDebugRef.current = this.sdkEventDebug;
57
72
  this.sdkEventDebug?.recordContextSnapshot(context);
58
- if (
59
- (await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug)) ===
60
- "stream_ended"
61
- ) {
62
- return;
73
+ // Resolved once here, before any drain await, so the drain decision and the
74
+ // prepare dispatch below always act on the same config snapshot.
75
+ const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
76
+ if (resolvedConfig.runtime.value === "local") {
77
+ if (
78
+ (await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug)) ===
79
+ "stream_ended"
80
+ ) {
81
+ return;
82
+ }
63
83
  }
64
84
  this.throwIfAborted();
65
85
 
@@ -70,6 +90,7 @@ export class CursorProviderTurnRunner {
70
90
  resolvedApiKey: this.resolvedApiKey,
71
91
  sdkEventDebug: this.sdkEventDebug,
72
92
  throwIfAborted: () => this.throwIfAborted(),
93
+ resolvedConfig,
73
94
  });
74
95
 
75
96
  sendResult = await sendCursorProviderTurn({
@@ -78,21 +99,23 @@ export class CursorProviderTurnRunner {
78
99
  sdkEventDebug: this.sdkEventDebug,
79
100
  sdkProcessErrorGuard,
80
101
  throwIfAborted: () => this.throwIfAborted(),
102
+ resolvedApiKey: this.resolvedApiKey,
81
103
  });
82
104
  const { send } = sendResult;
83
105
 
84
106
  if (prepared.runtime.kind === "live") {
107
+ const livePrepared = requireLocalLivePreparedTurn(prepared);
85
108
  liveCompletion = runFinalizer.startLiveRunCompletion({
86
109
  send,
87
- prepared,
110
+ prepared: livePrepared,
88
111
  modelId: model.id,
89
- discardIncompleteTools: (outcome) => discardIncompleteToolsFromPrepared(prepared, outcome),
112
+ discardIncompleteTools: (outcome) => discardIncompleteToolsFromPrepared(livePrepared, outcome),
90
113
  });
91
114
  await emitCursorLiveTurn({
92
115
  params: this.params,
93
- prepared,
116
+ prepared: livePrepared,
94
117
  sdkEventDebug: this.sdkEventDebug,
95
- discardIncompleteTools: (outcome) => discardIncompleteToolsFromPrepared(prepared, outcome),
118
+ discardIncompleteTools: (outcome) => discardIncompleteToolsFromPrepared(livePrepared, outcome),
96
119
  });
97
120
  return;
98
121
  }
@@ -110,9 +133,14 @@ export class CursorProviderTurnRunner {
110
133
  sdkEventDebug: this.sdkEventDebug,
111
134
  contextWindowAgentId: prepared.contextWindowAgentId,
112
135
  });
113
- prepared.sessionAgentLease.trackRunCompletion(outcomePromise);
114
- const outcome = await outcomePromise;
115
- await runFinalizer.applyTerminalEvent({ kind: "direct", prepared, outcome });
136
+ prepared.lifecycle.trackRunCompletion(outcomePromise);
137
+ const finalized = await outcomePromise;
138
+ await runFinalizer.applyTerminalEvent({
139
+ kind: "direct",
140
+ prepared,
141
+ outcome: finalized.outcome,
142
+ displayOnlyTraceBlock: finalized.displayOnlyTraceBlock,
143
+ });
116
144
  } catch (error) {
117
145
  await runFinalizer.applyTerminalEvent({ kind: "error", prepared, error });
118
146
  } finally {
@@ -1,7 +1,12 @@
1
1
  import type { SendOptions } from "@cursor/sdk";
2
2
  import { countCursorAgentMessages } from "./cursor-agent-message-web-tools.js";
3
+ import {
4
+ createCursorCloudLifecyclePersistenceError,
5
+ recordCursorCloudLifecycleSafely,
6
+ } from "./cursor-cloud-lifecycle.js";
3
7
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
4
8
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
9
+ import { consumeCursorLocalForceOverride } from "./cursor-runtime-state.js";
5
10
  import type { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
6
11
  import type {
7
12
  CursorProviderTurnRunnerParams,
@@ -16,10 +21,37 @@ export interface SendCursorProviderTurnParams {
16
21
  sdkEventDebug: CursorSdkEventDebugSink | undefined;
17
22
  sdkProcessErrorGuard: ReturnType<typeof installCursorSdkProcessErrorGuard>;
18
23
  throwIfAborted: () => void;
24
+ resolvedApiKey?: string;
25
+ }
26
+
27
+ const CLOUD_LEDGER_CANCEL_TIMEOUT_MS = 5000;
28
+
29
+ async function requestBoundedCloudRunCancellation(run: Awaited<ReturnType<CursorProviderTurnPrepareResult["agent"]["send"]>>): Promise<boolean> {
30
+ let timer: ReturnType<typeof setTimeout> | undefined;
31
+ const timeout = new Promise<false>((resolve) => {
32
+ timer = setTimeout(() => resolve(false), CLOUD_LEDGER_CANCEL_TIMEOUT_MS);
33
+ timer.unref?.();
34
+ });
35
+ try {
36
+ return await Promise.race([
37
+ Promise.resolve().then(() => run.cancel()).then(() => true, () => false),
38
+ timeout,
39
+ ]);
40
+ } finally {
41
+ if (timer) clearTimeout(timer);
42
+ }
43
+ }
44
+
45
+ function recordDebug(action: () => void): void {
46
+ try {
47
+ action();
48
+ } catch {
49
+ // Debug capture is optional and must never change provider execution.
50
+ }
19
51
  }
20
52
 
21
53
  export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnParams): Promise<CursorProviderTurnSendResult> {
22
- const { params, prepared, sdkEventDebug, sdkProcessErrorGuard, throwIfAborted } = sendParams;
54
+ const { params, prepared, sdkEventDebug, sdkProcessErrorGuard, throwIfAborted, resolvedApiKey } = sendParams;
23
55
  const { options } = params;
24
56
  const { agent, cwd, payload, meta, runtime } = prepared;
25
57
  const { turnCoordinator, liveRun } = runtime;
@@ -42,13 +74,15 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
42
74
  abortRegistration?.signal.addEventListener("abort", abortListener, { once: true });
43
75
  throwIfAborted();
44
76
  let cursorAgentMessageOffset: number | undefined;
45
- try {
46
- cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd);
47
- } catch (error) {
48
- sdkEventDebug?.recordError("cursor_agent_message_count", error);
77
+ if (prepared.runtimeTarget === "local") {
78
+ try {
79
+ cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd);
80
+ } catch (error) {
81
+ recordDebug(() => sdkEventDebug?.recordError("cursor_agent_message_count", error));
82
+ }
49
83
  }
50
84
  throwIfAborted();
51
- sdkEventDebug?.recordSendMeta({
85
+ recordDebug(() => sdkEventDebug?.recordSendMeta({
52
86
  mode: meta.sendPlan.mode,
53
87
  reason: meta.sendPlan.reason,
54
88
  resetAgent: meta.sendPlan.resetAgent,
@@ -60,35 +94,44 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
60
94
  nativeReplayId: meta.nativeReplayId,
61
95
  promptInputTokens: meta.promptInputTokens,
62
96
  agentMode: meta.agentMode,
63
- });
64
- sdkEventDebug?.recordSendPayload(payload);
65
- sdkEventDebug?.recordProviderEvent("agent_send_start", payload);
97
+ }));
98
+ recordDebug(() => sdkEventDebug?.recordSendPayload(payload));
99
+ recordDebug(() => sdkEventDebug?.recordProviderEvent("agent_send_start", payload));
66
100
  const sendOptions: SendOptions = {
67
101
  mode: meta.agentMode,
102
+ model: meta.modelSelection,
68
103
  onDelta: (args) => {
69
- sdkEventDebug?.recordOnDelta(args.update);
104
+ recordDebug(() => sdkEventDebug?.recordOnDelta(args.update));
70
105
  turnCoordinator.handleDelta(args.update);
71
106
  },
72
107
  onStep: (args) => {
73
- sdkEventDebug?.recordOnStep(args.step);
108
+ recordDebug(() => sdkEventDebug?.recordOnStep(args.step));
74
109
  turnCoordinator.handleStep(args.step);
75
110
  },
76
111
  };
112
+ throwIfAborted();
113
+ if (prepared.runtimeTarget === "local" && consumeCursorLocalForceOverride(prepared.localForce)) {
114
+ sendOptions.local = { force: true };
115
+ }
77
116
  const run = await agent.send(payload, sendOptions);
78
117
  sdkRun = run;
79
- sdkEventDebug?.recordRunMeta({
118
+ if (prepared.runtimeTarget === "cloud" && !recordCursorCloudLifecycleSafely({ agentId: run.agentId, runId: run.id }, resolvedApiKey)) {
119
+ const cancellationConfirmed = await requestBoundedCloudRunCancellation(run);
120
+ throw createCursorCloudLifecyclePersistenceError(run.agentId, "run", cancellationConfirmed, resolvedApiKey);
121
+ }
122
+ recordDebug(() => sdkEventDebug?.recordRunMeta({
80
123
  runId: run.id,
81
124
  requestId: run.requestId,
82
125
  agentId: run.agentId,
83
126
  status: run.status,
84
- });
85
- sdkEventDebug?.attachRunStream(run);
86
- sdkEventDebug?.recordProviderEvent("agent_send_returned", {
127
+ }));
128
+ recordDebug(() => sdkEventDebug?.attachRunStream(run));
129
+ recordDebug(() => sdkEventDebug?.recordProviderEvent("agent_send_returned", {
87
130
  runId: run.id,
88
131
  requestId: run.requestId,
89
132
  agentId: run.agentId,
90
133
  status: run.status,
91
- });
134
+ }));
92
135
  if (liveRun) cursorLiveRuns.attachSdkRun(liveRun, run);
93
136
  if (options?.signal?.aborted) {
94
137
  sdkProcessErrorGuard.suppressAbortErrors();
@@ -6,13 +6,14 @@ import type {
6
6
  Model,
7
7
  SimpleStreamOptions,
8
8
  } from "@earendil-works/pi-ai/compat";
9
- import type { AgentModeOption, SDKAgent, SDKImage } from "@cursor/sdk";
9
+ import type { AgentModeOption, ModelSelection, SDKAgent, SDKImage } from "@cursor/sdk";
10
10
  import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
11
11
  import type { SessionCursorAgentLease } from "./cursor-session-agent.js";
12
12
  import type { planCursorSessionSend } from "./cursor-session-agent.js";
13
13
  import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
14
14
  import type { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
15
15
  import type { CursorPrompt } from "./context.js";
16
+ import type { CursorResolvedSetting } from "./cursor-config.js";
16
17
 
17
18
  export interface CursorProviderTurnRunnerParams {
18
19
  model: Model<Api>;
@@ -37,12 +38,28 @@ export interface CursorProviderTurnSendMeta {
37
38
  bridgeEnabled: boolean;
38
39
  nativeReplayId: string;
39
40
  agentMode: AgentModeOption;
41
+ modelSelection: ModelSelection;
42
+ resumeNotice?: string;
40
43
  }
41
44
 
42
45
  interface CursorProviderTurnRuntimeBase {
43
46
  turnCoordinator: CursorSdkTurnCoordinator;
44
47
  }
45
48
 
49
+ /**
50
+ * Runtime-agnostic lifecycle operations for a prepared turn.
51
+ *
52
+ * Local implementations delegate to the session agent lease; cloud
53
+ * implementations no-op the local-only operations (commitSend,
54
+ * trackRunCompletion, abandon) and dispose the cloud agent instead.
55
+ */
56
+ export interface CursorProviderTurnLifecycle {
57
+ trackRunCompletion(completion: Promise<unknown>): void;
58
+ commitSend(context: Context, bootstrapped: boolean): void;
59
+ abandon(): Promise<void>;
60
+ dispose(): Promise<void>;
61
+ }
62
+
46
63
  export interface DirectCursorProviderTurnRuntime extends CursorProviderTurnRuntimeBase {
47
64
  kind: "direct";
48
65
  liveRun?: undefined;
@@ -55,25 +72,42 @@ export interface LiveCursorProviderTurnRuntime extends CursorProviderTurnRuntime
55
72
 
56
73
  export type CursorProviderTurnRuntime = DirectCursorProviderTurnRuntime | LiveCursorProviderTurnRuntime;
57
74
 
58
- /**
59
- * Single owned model for a prepared provider turn.
60
- *
61
- * Send, finalize, and cleanup phases receive this immutable object instead of
62
- * keeping parallel liveRun/turnCoordinator/resource bags in sync by convention.
63
- */
64
- export interface CursorProviderTurnPrepareResult {
75
+ interface CursorProviderTurnPrepareResultBase {
65
76
  agent: SDKAgent;
66
77
  cwd: string;
67
78
  payload: CursorProviderTurnSendPayload;
68
79
  meta: CursorProviderTurnSendMeta;
69
80
  contextWindowAgentId: string;
70
81
  textDeltas: string[];
71
- sessionAgentScopeKey: string;
72
- sessionAgentLease: SessionCursorAgentLease;
73
82
  restoreCursorSdkOutputFilter: () => void;
83
+ lifecycle: CursorProviderTurnLifecycle;
84
+ }
85
+
86
+ export interface LocalCursorProviderTurnPrepareResult extends CursorProviderTurnPrepareResultBase {
87
+ runtimeTarget: "local";
74
88
  runtime: CursorProviderTurnRuntime;
89
+ sessionAgentScopeKey: string;
90
+ sessionAgentLease: SessionCursorAgentLease;
91
+ localForce: CursorResolvedSetting<boolean>;
75
92
  }
76
93
 
94
+ export interface CloudCursorProviderTurnPrepareResult extends CursorProviderTurnPrepareResultBase {
95
+ runtimeTarget: "cloud";
96
+ runtime: DirectCursorProviderTurnRuntime;
97
+ sessionAgentScopeKey?: undefined;
98
+ sessionAgentLease?: undefined;
99
+ }
100
+
101
+ /**
102
+ * Single owned model for a prepared provider turn.
103
+ *
104
+ * Send, finalize, and cleanup phases receive this immutable object instead of
105
+ * keeping parallel liveRun/turnCoordinator/resource bags in sync by convention.
106
+ */
107
+ export type CursorProviderTurnPrepareResult =
108
+ | LocalCursorProviderTurnPrepareResult
109
+ | CloudCursorProviderTurnPrepareResult;
110
+
77
111
  export interface CursorProviderTurnSend {
78
112
  run: Awaited<ReturnType<SDKAgent["send"]>>;
79
113
  cursorAgentMessageOffset: number | undefined;