pi-cursor-sdk 0.4.0 → 0.5.0

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 (125) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +65 -13
  3. package/dist/context-window-cache.js +0 -6
  4. package/dist/context.js +8 -0
  5. package/dist/cursor-cloud-lifecycle.js +122 -98
  6. package/dist/cursor-durable-fs.js +97 -3
  7. package/dist/cursor-fallback-models.generated.js +288 -1
  8. package/dist/cursor-fallback-warning.js +2 -2
  9. package/dist/cursor-http1.js +8 -8
  10. package/dist/cursor-live-run-coordinator.js +9 -13
  11. package/dist/cursor-native-replay-routing.js +1 -1
  12. package/dist/cursor-native-tool-display-registration.js +46 -22
  13. package/dist/cursor-native-tool-display-state.js +57 -22
  14. package/dist/cursor-native-tool-display-tools.js +5 -6
  15. package/dist/cursor-pi-tool-bridge-constants.js +23 -2
  16. package/dist/cursor-pi-tool-bridge-run.js +7 -3
  17. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  18. package/dist/cursor-pi-tool-bridge.js +31 -9
  19. package/dist/cursor-provider-binding.js +58 -0
  20. package/dist/cursor-provider-errors.js +28 -7
  21. package/dist/cursor-provider-lazy.js +23 -11
  22. package/dist/cursor-provider-live-run-drain.js +15 -12
  23. package/dist/cursor-provider-run-finalizer.js +19 -8
  24. package/dist/cursor-provider-turn-coordinator.js +1 -0
  25. package/dist/cursor-provider-turn-display-router.js +4 -0
  26. package/dist/cursor-provider-turn-emit.js +1 -0
  27. package/dist/cursor-provider-turn-finalize.js +29 -20
  28. package/dist/cursor-provider-turn-prepare.js +24 -11
  29. package/dist/cursor-provider-turn-runner.js +37 -11
  30. package/dist/cursor-provider-turn-send.js +22 -9
  31. package/dist/cursor-provider-turn-summary.js +89 -0
  32. package/dist/cursor-provider.js +9 -3
  33. package/dist/cursor-request-provenance.js +42 -0
  34. package/dist/cursor-ripgrep-path.js +14 -3
  35. package/dist/cursor-runtime-state.js +50 -43
  36. package/dist/cursor-sdk-billed-usage.js +54 -75
  37. package/dist/cursor-sdk-event-debug-session.js +5 -6
  38. package/dist/cursor-sdk-event-debug.js +9 -6
  39. package/dist/cursor-sdk-process-error-guard.js +8 -7
  40. package/dist/cursor-sdk-runtime.js +13 -0
  41. package/dist/cursor-session-agent-cleanup.js +2 -2
  42. package/dist/cursor-session-agent-lifecycle.js +18 -9
  43. package/dist/cursor-session-agent-lineage.js +28 -12
  44. package/dist/cursor-session-agent-resume.js +51 -35
  45. package/dist/cursor-session-agent.js +9 -9
  46. package/dist/cursor-session-compaction-prep.js +2 -7
  47. package/dist/cursor-session-scope.js +71 -39
  48. package/dist/cursor-session-settings.js +61 -0
  49. package/dist/cursor-skill-tool.js +32 -49
  50. package/dist/cursor-state.js +72 -62
  51. package/dist/cursor-usage-accounting.js +13 -70
  52. package/dist/cursor-usage-command.js +89 -0
  53. package/dist/cursor-usage-ledger.js +512 -0
  54. package/dist/index.js +11 -19
  55. package/dist/model-discovery.js +11 -19
  56. package/docs/cursor-dogfood-checklist.md +3 -3
  57. package/docs/cursor-live-smoke-checklist.md +15 -15
  58. package/docs/cursor-model-ux-spec.md +59 -18
  59. package/docs/cursor-native-tool-replay.md +5 -3
  60. package/docs/cursor-native-tool-visual-audit.md +8 -8
  61. package/docs/cursor-testing-lessons.md +47 -23
  62. package/docs/cursor-tool-surfaces.md +1 -1
  63. package/docs/platform-smoke-implementation.md +10 -6
  64. package/docs/platform-smoke.md +35 -46
  65. package/package.json +6 -6
  66. package/scripts/cloud-runtime-smoke.mjs +4 -2
  67. package/scripts/local-resume-smoke.mjs +1 -1
  68. package/scripts/platform-smoke/card-detect.mjs +2 -8
  69. package/scripts/platform-smoke/targets.mjs +8 -15
  70. package/scripts/platform-smoke/visual-evidence.mjs +7 -2
  71. package/scripts/tmux-live-smoke.sh +57 -90
  72. package/src/context-window-cache.ts +0 -6
  73. package/src/context.ts +8 -0
  74. package/src/cursor-cloud-lifecycle.ts +124 -98
  75. package/src/cursor-durable-fs.ts +65 -3
  76. package/src/cursor-fallback-models.generated.ts +288 -1
  77. package/src/cursor-fallback-warning.ts +2 -2
  78. package/src/cursor-http1.ts +8 -8
  79. package/src/cursor-live-run-coordinator.ts +12 -13
  80. package/src/cursor-native-replay-routing.ts +2 -1
  81. package/src/cursor-native-tool-display-registration.ts +48 -24
  82. package/src/cursor-native-tool-display-state.ts +65 -21
  83. package/src/cursor-native-tool-display-tools.ts +7 -4
  84. package/src/cursor-pi-tool-bridge-constants.ts +27 -2
  85. package/src/cursor-pi-tool-bridge-run.ts +11 -3
  86. package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
  87. package/src/cursor-pi-tool-bridge.ts +26 -9
  88. package/src/cursor-provider-binding.ts +62 -0
  89. package/src/cursor-provider-errors.ts +27 -7
  90. package/src/cursor-provider-lazy.ts +21 -15
  91. package/src/cursor-provider-live-run-drain.ts +20 -13
  92. package/src/cursor-provider-run-finalizer.ts +24 -15
  93. package/src/cursor-provider-turn-coordinator.ts +2 -0
  94. package/src/cursor-provider-turn-display-router.ts +5 -0
  95. package/src/cursor-provider-turn-emit.ts +3 -1
  96. package/src/cursor-provider-turn-finalize.ts +38 -23
  97. package/src/cursor-provider-turn-prepare.ts +23 -14
  98. package/src/cursor-provider-turn-runner.ts +40 -13
  99. package/src/cursor-provider-turn-send.ts +23 -15
  100. package/src/cursor-provider-turn-summary.ts +94 -0
  101. package/src/cursor-provider-turn-types.ts +37 -4
  102. package/src/cursor-provider.ts +25 -3
  103. package/src/cursor-request-provenance.ts +64 -0
  104. package/src/cursor-ripgrep-path.ts +15 -5
  105. package/src/cursor-runtime-state.ts +53 -48
  106. package/src/cursor-sdk-billed-usage.ts +54 -102
  107. package/src/cursor-sdk-event-debug-session.ts +5 -5
  108. package/src/cursor-sdk-event-debug.ts +10 -6
  109. package/src/cursor-sdk-process-error-guard.ts +7 -7
  110. package/src/cursor-sdk-runtime.ts +14 -0
  111. package/src/cursor-session-agent-cleanup.ts +2 -2
  112. package/src/cursor-session-agent-lifecycle.ts +14 -9
  113. package/src/cursor-session-agent-lineage.ts +26 -12
  114. package/src/cursor-session-agent-resume.ts +49 -36
  115. package/src/cursor-session-agent.ts +13 -10
  116. package/src/cursor-session-compaction-prep.ts +2 -7
  117. package/src/cursor-session-scope.ts +87 -39
  118. package/src/cursor-session-settings.ts +72 -0
  119. package/src/cursor-skill-tool.ts +37 -61
  120. package/src/cursor-state.ts +74 -67
  121. package/src/cursor-usage-accounting.ts +17 -82
  122. package/src/cursor-usage-command.ts +76 -0
  123. package/src/cursor-usage-ledger.ts +500 -0
  124. package/src/index.ts +14 -23
  125. package/src/model-discovery.ts +14 -22
@@ -1,11 +1,9 @@
1
1
  import { loadCursorTranscriptWebToolCallsAfterOffset } from "./cursor-agent-message-web-tools.js";
2
2
  import { collectCursorCloudRunReport, formatCursorCloudRunReport, } from "./cursor-cloud-reporting.js";
3
- import { recordCursorCloudLifecycleRun } from "./cursor-cloud-lifecycle.js";
4
3
  import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
5
4
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
6
5
  import { isCursorRunFinishedSuccessfully, resolveCursorRunOutcome, } from "./cursor-provider-run-outcome.js";
7
6
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
8
- import { attachCursorSdkBilledTurnUsage } from "./cursor-sdk-billed-usage.js";
9
7
  export async function cacheSdkContextWindow(agentId, modelId, cwd, store) {
10
8
  try {
11
9
  const { createAgentPlatform } = await loadCursorSdk();
@@ -74,10 +72,30 @@ function recordCursorCloudReportingError(sdkEventDebug, error, apiKey) {
74
72
  // Debug reporting must never affect provider execution.
75
73
  }
76
74
  }
77
- /** Single wait/finalize path for SDK runs: wait, debug capture, transcript replay, incomplete tools, artifacts, context cache. */
75
+ export async function recordCursorProviderTerminalUsage(prepared, status, waitUsage) {
76
+ if (prepared.runtime.usageTerminalRecorded)
77
+ return undefined;
78
+ prepared.runtime.usageTerminalRecorded = true;
79
+ try {
80
+ return await prepared.usage.recordTerminal({ status, waitUsage, handleUsage: prepared.runtime.sdkRun?.usage });
81
+ }
82
+ catch (error) {
83
+ prepared.usage.notePersistenceFailure(error);
84
+ return undefined;
85
+ }
86
+ }
87
+ /** Single wait/finalize path for SDK runs: accounting, replay, incomplete tools, artifacts, context cache. */
78
88
  export async function awaitFinalizeCursorRunOutcome(params) {
79
89
  const apiKey = params.resolvedApiKey ?? params.optionsApiKey;
80
- const waitResult = params.waitResult ?? (await params.run.wait());
90
+ params.prepared.runtime.sdkRun = params.run;
91
+ let waitResult;
92
+ try {
93
+ waitResult = params.waitResult ?? (await params.run.wait());
94
+ }
95
+ catch (error) {
96
+ await recordCursorProviderTerminalUsage(params.prepared, params.signal?.aborted ? "abort" : "error");
97
+ throw error;
98
+ }
81
99
  const outcome = buildCursorRunOutcomeFromWait({
82
100
  waitResult,
83
101
  prepared: params.prepared,
@@ -87,16 +105,7 @@ export async function awaitFinalizeCursorRunOutcome(params) {
87
105
  resolvedApiKey: params.resolvedApiKey,
88
106
  optionsApiKey: params.optionsApiKey,
89
107
  });
90
- const billed = await attachCursorSdkBilledTurnUsage({
91
- agent: params.prepared.agent,
92
- agentId: params.run.agentId,
93
- runtime: params.prepared.runtimeTarget,
94
- runId: params.run.id,
95
- });
96
- params.prepared.runtime.billedTurnUsage = billed.turn;
97
- if (params.prepared.runtime.liveRun) {
98
- params.prepared.runtime.liveRun.billedTurnUsage = billed.turn;
99
- }
108
+ const billing = await recordCursorProviderTerminalUsage(params.prepared, outcome.kind === "finished" ? "success" : outcome.kind === "cancelled" ? "abort" : "error", waitResult.usage);
100
109
  let displayOnlyTraceBlock;
101
110
  if (params.prepared.runtimeTarget === "cloud" && isCursorRunFinishedSuccessfully(outcome)) {
102
111
  let report = { agentId: params.run.agentId, runId: params.run.id, branches: [] };
@@ -106,14 +115,14 @@ export async function awaitFinalizeCursorRunOutcome(params) {
106
115
  run: params.run,
107
116
  waitResult,
108
117
  apiKey,
109
- agentUsage: billed.agentUsage,
118
+ agentUsage: billing ? billing.status === "observed-pending-settlement" ? billing.snapshot : null : undefined,
110
119
  });
111
120
  }
112
121
  catch (error) {
113
122
  recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
114
123
  }
115
124
  try {
116
- recordCursorCloudLifecycleRun(report, { apiKey });
125
+ params.prepared.recordCloudLifecycle(report, apiKey);
117
126
  }
118
127
  catch (error) {
119
128
  recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
@@ -137,8 +146,8 @@ export async function awaitFinalizeCursorRunOutcome(params) {
137
146
  catch {
138
147
  // Debug reporting must never affect provider execution.
139
148
  }
140
- if (params.prepared.runtimeTarget === "local" && isCursorRunFinishedSuccessfully(outcome)) {
141
- await replayCursorTranscriptWebToolCalls(params.run.agentId, params.prepared.cwd, params.cursorAgentMessageOffset, params.prepared.sessionAgentLease.store, params.prepared.runtime.turnCoordinator, params.sdkEventDebug);
149
+ if (params.prepared.runtimeTarget === "local" && params.prepared.execution === "conversation" && isCursorRunFinishedSuccessfully(outcome)) {
150
+ await replayCursorTranscriptWebToolCalls(params.run.agentId, params.prepared.cwd, params.cursorAgentMessageOffset, params.prepared.store, params.prepared.runtime.turnCoordinator, params.sdkEventDebug);
142
151
  }
143
152
  params.prepared.runtime.turnCoordinator.discardIncompleteStartedToolCalls(outcome.incompleteTools);
144
153
  try {
@@ -147,8 +156,8 @@ export async function awaitFinalizeCursorRunOutcome(params) {
147
156
  catch {
148
157
  // Debug artifact failures must never affect provider execution.
149
158
  }
150
- if (params.prepared.runtimeTarget === "local" && params.cacheContextWindow !== false) {
151
- await cacheSdkContextWindow(params.contextWindowAgentId ?? params.run.agentId, params.modelId, params.prepared.cwd, params.prepared.sessionAgentLease.store);
159
+ if (params.prepared.runtimeTarget === "local" && params.prepared.execution === "conversation" && params.cacheContextWindow !== false) {
160
+ await cacheSdkContextWindow(params.contextWindowAgentId ?? params.run.agentId, params.modelId, params.prepared.cwd, params.prepared.store);
152
161
  }
153
162
  return { outcome, displayOnlyTraceBlock };
154
163
  }
@@ -7,22 +7,23 @@ import { acquireSessionCursorAgent, buildCursorSessionSendPrompt, planCursorSess
7
7
  import { buildCursorPrompt, estimateCursorPromptTokens } from "./context.js";
8
8
  import { getCursorPromptOptions } from "./cursor-usage-accounting.js";
9
9
  import { getActiveContextToolNames } from "./cursor-context-tools.js";
10
- import { abandonSessionCursorAgent, createCursorNativeReplayId, cursorLiveRuns, getActiveCursorLiveRunForCurrentScope, getPendingCursorLiveRun, } from "./cursor-provider-live-run-drain.js";
10
+ import { abandonSessionCursorAgent, createCursorNativeReplayId, cursorLiveRuns, getActiveCursorLiveRunForCurrentScope, } from "./cursor-provider-live-run-drain.js";
11
11
  import { getCursorProviderAgentModeOrThrow, getEffectiveFastForModelId, } from "./cursor-state.js";
12
12
  import { resolveEffectiveCursorConfig } from "./cursor-runtime-state.js";
13
13
  import { buildCursorModelSelection } from "./model-discovery.js";
14
14
  import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
15
15
  import { formatCursorCloudPreflightError, buildCursorCloudAgentOptions, preflightCursorCloudRuntime, } from "./cursor-cloud-options.js";
16
16
  import { inspectCursorCloudLocalState } from "./cursor-cloud-local-state.js";
17
- import { getCursorSessionName, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
17
+ import { getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
18
18
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
19
19
  import { buildCursorToolManifestText, resolveCursorToolManifestEnabled, } from "./cursor-tool-manifest.js";
20
20
  import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display-state.js";
21
- import { createCursorCloudLifecyclePersistenceError, recordCursorCloudLifecycleSafely, } from "./cursor-cloud-lifecycle.js";
21
+ import { createCursorCloudLifecyclePersistenceError } from "./cursor-cloud-lifecycle.js";
22
22
  import { MISSING_CURSOR_API_KEY_MESSAGE } from "./cursor-provider-errors.js";
23
23
  import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
24
24
  import { resolveCursorApiKey } from "./cursor-api-key.js";
25
25
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
26
+ import { prepareCursorSummaryProviderTurn } from "./cursor-provider-turn-summary.js";
26
27
  function buildCursorCloudPromptContext(context, handoff) {
27
28
  if (handoff === "bootstrap")
28
29
  return context;
@@ -38,8 +39,8 @@ function buildCursorCloudPromptContext(context, handoff) {
38
39
  return { ...current, messages: messages.slice(-1) };
39
40
  }
40
41
  const CLOUD_SEND_PLAN = { mode: "bootstrap", resetAgent: false, reason: "initial" };
41
- export function resolveCursorProviderTurnConfig(cwd) {
42
- return resolveEffectiveCursorConfig({ cwd, projectTrusted: getCursorSessionProjectTrusted() });
42
+ export function resolveCursorProviderTurnConfig(cwd, projectTrusted = getCursorSessionProjectTrusted(), scopeKey) {
43
+ return resolveEffectiveCursorConfig({ cwd, projectTrusted, scopeKey });
43
44
  }
44
45
  function buildCloudCursorProviderTurnLifecycle(agent) {
45
46
  return {
@@ -78,7 +79,7 @@ async function prepareCursorCloudProviderTurn(prepareParams) {
78
79
  });
79
80
  if (!preflight.ok)
80
81
  throw new Error(formatCursorCloudPreflightError(preflight));
81
- if (getPendingCursorLiveRun(context) || getActiveCursorLiveRunForCurrentScope()) {
82
+ if (getActiveCursorLiveRunForCurrentScope(params.scope.scopeKey)) {
82
83
  throw new Error("Cursor cloud runtime cannot start while a local Cursor live run is pending; finish or abort the local run, then retry.");
83
84
  }
84
85
  const { Agent } = await loadCursorSdk();
@@ -96,10 +97,10 @@ async function prepareCursorCloudProviderTurn(prepareParams) {
96
97
  modelSelection: selection,
97
98
  agentMode,
98
99
  resolvedConfig,
99
- name: getCursorSessionName(),
100
+ name: params.scope.sessionName,
100
101
  })));
101
102
  cloudAgentForCleanup = agent;
102
- if (!recordCursorCloudLifecycleSafely({ agentId: agent.agentId }, resolvedApiKey)) {
103
+ if (!params.recordCloudLifecycle({ agentId: agent.agentId }, resolvedApiKey)) {
103
104
  throw createCursorCloudLifecyclePersistenceError(agent.agentId, "intent", undefined, resolvedApiKey);
104
105
  }
105
106
  sdkEventDebug?.recordProviderMeta({ runtime: "cloud", cloudAgentId: agent.agentId, phase: "agent_created" });
@@ -137,6 +138,8 @@ async function prepareCursorCloudProviderTurn(prepareParams) {
137
138
  cloudAgentForCleanup = undefined;
138
139
  return {
139
140
  runtimeTarget: "cloud",
141
+ execution: "conversation",
142
+ recordCloudLifecycle: params.recordCloudLifecycle,
140
143
  agent,
141
144
  cwd,
142
145
  payload: {
@@ -190,6 +193,8 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
190
193
  const queuedBridgeRequestsBeforeLiveRun = [];
191
194
  let liveRunForBridgeQueue;
192
195
  const sessionAgentAcquireParams = {
196
+ scope: params.scope,
197
+ bridge: params.bridge,
193
198
  apiKey: resolvedApiKey,
194
199
  agentMode,
195
200
  cwd,
@@ -258,7 +263,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
258
263
  };
259
264
  const sessionBridgeRun = bridgeRun;
260
265
  const promptInputTokens = estimateCursorPromptTokens(prompt, promptOptions);
261
- const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
266
+ const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled(params.nativeDisplay);
262
267
  const activeToolNames = getActiveContextToolNames(context);
263
268
  sdkEventDebug?.recordProviderMeta({
264
269
  model: {
@@ -295,6 +300,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
295
300
  promptInputTokens,
296
301
  textDeltas,
297
302
  debugRecorder: sdkEventDebug,
303
+ nativeDisplay: params.nativeDisplay,
298
304
  })
299
305
  : undefined;
300
306
  if (liveRun) {
@@ -311,6 +317,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
311
317
  liveRun,
312
318
  useNativeToolReplay,
313
319
  activeToolNames,
320
+ registeredToolNames: params.nativeDisplay ? new Set(params.nativeDisplay.registeredNativeToolSources.keys()) : undefined,
314
321
  nativeReplayId,
315
322
  textDeltas,
316
323
  debugRecorder: sdkEventDebug,
@@ -318,6 +325,9 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
318
325
  completed = true;
319
326
  return {
320
327
  runtimeTarget: "local",
328
+ execution: "conversation",
329
+ store: sessionAgentLease.store,
330
+ storeIdentity: sessionAgentLease.storeIdentity,
321
331
  agent,
322
332
  cwd,
323
333
  payload: sendPayload,
@@ -363,9 +373,12 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
363
373
  export async function prepareCursorProviderTurn(prepareParams) {
364
374
  const { params, resolvedConfig } = prepareParams;
365
375
  const { model, options } = params;
366
- const agentMode = getCursorProviderAgentModeOrThrow();
367
- const fastEnabled = resolvedConfig.runtime.value === "cloud" ? undefined : getEffectiveFastForModelId(model.id);
376
+ const fastEnabled = resolvedConfig.runtime.value === "cloud" ? undefined : getEffectiveFastForModelId(model.id, params.scope.scopeKey);
368
377
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
378
+ if (resolvedConfig.runtime.value === "local" && params.request.purpose !== "normal") {
379
+ return prepareCursorSummaryProviderTurn(prepareParams, selection);
380
+ }
381
+ const agentMode = getCursorProviderAgentModeOrThrow(params.scope.scopeKey);
369
382
  const context = { ...prepareParams, agentMode, selection, fastEnabled };
370
383
  return resolvedConfig.runtime.value === "cloud"
371
384
  ? prepareCursorCloudProviderTurn(context)
@@ -1,6 +1,5 @@
1
1
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
2
2
  import { drainExistingCursorLiveRunBeforeSend } from "./cursor-provider-live-run-drain.js";
3
- import { getCursorSessionCwd } from "./cursor-session-scope.js";
4
3
  import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
5
4
  import { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
6
5
  import { awaitFinalizeCursorRunOutcome } from "./cursor-provider-turn-finalize.js";
@@ -9,7 +8,7 @@ import { CursorRunFinalizer } from "./cursor-provider-run-finalizer.js";
9
8
  import { prepareCursorProviderTurn, requireCursorApiKey, resolveCursorProviderTurnConfig, } from "./cursor-provider-turn-prepare.js";
10
9
  import { sendCursorProviderTurn } from "./cursor-provider-turn-send.js";
11
10
  function requireLocalLivePreparedTurn(prepared) {
12
- if (prepared.runtimeTarget !== "local" || prepared.runtime.kind !== "live") {
11
+ if (prepared.runtimeTarget !== "local" || prepared.execution !== "conversation" || prepared.runtime.kind !== "live") {
13
12
  throw new Error("Cursor live run requires a local live prepared turn");
14
13
  }
15
14
  return prepared;
@@ -32,6 +31,7 @@ export class CursorProviderTurnRunner {
32
31
  async run(sdkProcessErrorGuard) {
33
32
  const { stream, partial, model, context, options, sdkEventDebugRef } = this.params;
34
33
  let prepared;
34
+ let started;
35
35
  let sendResult;
36
36
  let liveCompletion;
37
37
  const runFinalizer = new CursorRunFinalizer({
@@ -43,20 +43,22 @@ export class CursorProviderTurnRunner {
43
43
  });
44
44
  try {
45
45
  this.throwIfAborted();
46
- const cwd = getCursorSessionCwd();
46
+ const { scope } = this.params;
47
+ const cwd = scope.cwd;
47
48
  this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
48
49
  cwd,
49
50
  modelId: model.id,
50
51
  provider: model.provider,
52
+ scope,
51
53
  });
52
54
  sdkEventDebugRef.current = this.sdkEventDebug;
53
55
  this.sdkEventDebug?.recordContextSnapshot(context);
54
56
  // Resolved once here, before any drain await, so the drain decision and the
55
57
  // prepare dispatch below always act on the same config snapshot.
56
- const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
58
+ const resolvedConfig = resolveCursorProviderTurnConfig(cwd, scope.projectTrusted, scope.scopeKey);
57
59
  this.runtimeTarget = resolvedConfig.runtime.value;
58
- if (resolvedConfig.runtime.value === "local") {
59
- if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug)) ===
60
+ if (resolvedConfig.runtime.value === "local" && this.params.request.purpose === "normal") {
61
+ if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug, scope.scopeKey, this.params.request.occupancyFloor)) ===
60
62
  "stream_ended") {
61
63
  return;
62
64
  }
@@ -71,9 +73,33 @@ export class CursorProviderTurnRunner {
71
73
  throwIfAborted: () => this.throwIfAborted(),
72
74
  resolvedConfig,
73
75
  });
76
+ const usage = await this.params.usageRecorder.start({
77
+ agent: prepared.agent,
78
+ runtime: prepared.runtimeTarget,
79
+ model: { id: model.id, provider: model.provider, cost: { ...model.cost } },
80
+ modelSelection: prepared.meta.modelSelection,
81
+ purpose: this.params.request.purpose,
82
+ ...(prepared.runtimeTarget === "local" ? { storeIdentity: prepared.storeIdentity.stateRoot } : {}),
83
+ resumed: prepared.runtimeTarget === "local" && prepared.execution === "conversation" && prepared.sessionAgentLease.resumed === true,
84
+ newlyCreated: prepared.runtimeTarget !== "local" || prepared.execution === "summary" || (prepared.sessionAgentLease.created && !prepared.sessionAgentLease.resumed),
85
+ }).catch(error => {
86
+ this.params.usageRecorder.notePersistenceFailure(error);
87
+ throw error;
88
+ });
89
+ started = { ...prepared, usage };
90
+ if (started.runtime.liveRun) {
91
+ started.runtime.liveRun.onAbandon = async () => {
92
+ try {
93
+ await usage.recordTerminal({ status: "abandon" });
94
+ }
95
+ catch (error) {
96
+ usage.notePersistenceFailure(error);
97
+ }
98
+ };
99
+ }
74
100
  sendResult = await sendCursorProviderTurn({
75
101
  params: this.params,
76
- prepared,
102
+ prepared: started,
77
103
  sdkEventDebug: this.sdkEventDebug,
78
104
  sdkProcessErrorGuard,
79
105
  throwIfAborted: () => this.throwIfAborted(),
@@ -81,7 +107,7 @@ export class CursorProviderTurnRunner {
81
107
  });
82
108
  const { send } = sendResult;
83
109
  if (prepared.runtime.kind === "live") {
84
- const livePrepared = requireLocalLivePreparedTurn(prepared);
110
+ const livePrepared = requireLocalLivePreparedTurn(started);
85
111
  liveCompletion = runFinalizer.startLiveRunCompletion({
86
112
  send,
87
113
  prepared: livePrepared,
@@ -98,7 +124,7 @@ export class CursorProviderTurnRunner {
98
124
  }
99
125
  const outcomePromise = awaitFinalizeCursorRunOutcome({
100
126
  run: send.run,
101
- prepared,
127
+ prepared: started,
102
128
  cursorAgentMessageOffset: send.cursorAgentMessageOffset,
103
129
  modelId: model.id,
104
130
  signal: options?.signal,
@@ -113,13 +139,13 @@ export class CursorProviderTurnRunner {
113
139
  const finalized = await outcomePromise;
114
140
  await runFinalizer.applyTerminalEvent({
115
141
  kind: "direct",
116
- prepared,
142
+ prepared: started,
117
143
  outcome: finalized.outcome,
118
144
  displayOnlyTraceBlock: finalized.displayOnlyTraceBlock,
119
145
  });
120
146
  }
121
147
  catch (error) {
122
- await runFinalizer.applyTerminalEvent({ kind: "error", prepared, error });
148
+ await runFinalizer.applyTerminalEvent({ kind: "error", prepared: started ?? prepared, error });
123
149
  }
124
150
  finally {
125
151
  await runFinalizer.cleanup(prepared, sendResult, liveCompletion);
@@ -1,6 +1,5 @@
1
1
  import { countCursorAgentMessages } from "./cursor-agent-message-web-tools.js";
2
- import { createCursorCloudLifecyclePersistenceError, recordCursorCloudLifecycleSafely, } from "./cursor-cloud-lifecycle.js";
3
- import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
2
+ import { createCursorCloudLifecyclePersistenceError } from "./cursor-cloud-lifecycle.js";
4
3
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
5
4
  import { consumeCursorLocalForceOverride } from "./cursor-runtime-state.js";
6
5
  import { recordCursorSessionAgentLineage } from "./cursor-session-agent-lineage.js";
@@ -52,9 +51,9 @@ export async function sendCursorProviderTurn(sendParams) {
52
51
  abortRegistration?.signal.addEventListener("abort", abortListener, { once: true });
53
52
  throwIfAborted();
54
53
  let cursorAgentMessageOffset;
55
- if (prepared.runtimeTarget === "local") {
54
+ if (prepared.runtimeTarget === "local" && prepared.execution === "conversation") {
56
55
  try {
57
- cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd, prepared.sessionAgentLease.store);
56
+ cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd, prepared.store);
58
57
  }
59
58
  catch (error) {
60
59
  recordDebug(() => sdkEventDebug?.recordError("cursor_agent_message_count", error));
@@ -80,6 +79,14 @@ export async function sendCursorProviderTurn(sendParams) {
80
79
  mode: meta.agentMode,
81
80
  model: meta.modelSelection,
82
81
  onDelta: (args) => {
82
+ if (args.update.type === "turn-ended") {
83
+ try {
84
+ prepared.usage.observeRawTurn(args.update.usage);
85
+ }
86
+ catch (error) {
87
+ params.usageRecorder.notePersistenceFailure(error);
88
+ }
89
+ }
83
90
  recordDebug(() => sdkEventDebug?.recordOnDelta(args.update));
84
91
  turnCoordinator.handleDelta(args.update);
85
92
  },
@@ -89,17 +96,24 @@ export async function sendCursorProviderTurn(sendParams) {
89
96
  },
90
97
  };
91
98
  throwIfAborted();
92
- if (prepared.runtimeTarget === "local" && consumeCursorLocalForceOverride(prepared.localForce)) {
99
+ if (prepared.runtimeTarget === "local" && prepared.execution === "conversation" && consumeCursorLocalForceOverride(prepared.localForce, params.scope.scopeKey)) {
93
100
  sendOptions.local = { force: true };
94
101
  }
95
102
  const runPromise = agent.send(payload, sendOptions);
96
103
  // Record at send initiation (promise created), including later reject/cancel paths.
97
- if (prepared.runtimeTarget === "local") {
98
- recordCursorSessionAgentLineage(agent.agentId);
104
+ if (prepared.runtimeTarget === "local" && prepared.execution === "conversation") {
105
+ recordCursorSessionAgentLineage(agent.agentId, params.scope.scopeKey);
99
106
  }
100
107
  const run = await runPromise;
101
108
  sdkRun = run;
102
- if (prepared.runtimeTarget === "cloud" && !recordCursorCloudLifecycleSafely({ agentId: run.agentId, runId: run.id }, resolvedApiKey)) {
109
+ runtime.sdkRun = run;
110
+ try {
111
+ await prepared.usage.recordRun({ runId: run.id, requestId: run.requestId });
112
+ }
113
+ catch (error) {
114
+ params.usageRecorder.notePersistenceFailure(error);
115
+ }
116
+ if (prepared.runtimeTarget === "cloud" && !params.recordCloudLifecycle({ agentId: run.agentId, runId: run.id }, resolvedApiKey)) {
103
117
  const cancellationConfirmed = await requestBoundedCloudRunCancellation(run);
104
118
  throw createCursorCloudLifecyclePersistenceError(run.agentId, "run", cancellationConfirmed, resolvedApiKey);
105
119
  }
@@ -122,7 +136,6 @@ export async function sendCursorProviderTurn(sendParams) {
122
136
  sdkProcessErrorGuard.suppressAbortErrors();
123
137
  liveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
124
138
  await run.cancel().catch(() => { });
125
- throw new CursorLiveRunAbortError();
126
139
  }
127
140
  completed = true;
128
141
  return {
@@ -0,0 +1,89 @@
1
+ import { buildCursorSummaryPrompt, estimateCursorPromptTokens } from "./context.js";
2
+ import { configureCursorSdkHttp1 } from "./cursor-http1.js";
3
+ import { createCursorNativeReplayId } from "./cursor-provider-live-run-drain.js";
4
+ import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
5
+ import { ensureCursorRipgrepPath } from "./cursor-ripgrep-path.js";
6
+ import { loadCursorSdk } from "./cursor-sdk-runtime.js";
7
+ import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
8
+ import { buildCursorLocalAgentOptions } from "./cursor-session-agent.js";
9
+ import { openCursorSessionStoreForScope } from "./cursor-session-store.js";
10
+ import { getCursorPromptOptions } from "./cursor-usage-accounting.js";
11
+ function summaryLifecycle(agent, sessionStore) {
12
+ let disposal;
13
+ const dispose = () => disposal ??= (async () => {
14
+ try {
15
+ await agent[Symbol.asyncDispose]();
16
+ }
17
+ finally {
18
+ await sessionStore.dispose();
19
+ }
20
+ })();
21
+ return { trackRunCompletion: () => { }, commitSend: () => { }, abandon: dispose, dispose };
22
+ }
23
+ /** A native summary never enters the conversation pool or inherits its tool/mode/resume state. */
24
+ export async function prepareCursorSummaryProviderTurn(prepareParams, selection) {
25
+ const { params, cwd, resolvedApiKey, resolvedConfig, sdkEventDebug, throwIfAborted } = prepareParams;
26
+ let sessionStore;
27
+ let agent;
28
+ let restoreCursorSdkOutputFilter;
29
+ let completed = false;
30
+ try {
31
+ ensureCursorRipgrepPath();
32
+ const sdk = await loadCursorSdk();
33
+ configureCursorSdkHttp1(sdk, resolvedConfig.local.useHttp1ForAgent);
34
+ restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
35
+ sessionStore = (await openCursorSessionStoreForScope({
36
+ cwd, scopeKey: params.scope.scopeKey, persistent: false, hasResumeHandle: false,
37
+ })).sessionStore;
38
+ throwIfAborted();
39
+ agent = await suppressCursorSdkOutput(() => sdk.Agent.create({
40
+ apiKey: resolvedApiKey,
41
+ model: selection,
42
+ mode: "agent",
43
+ tools: [],
44
+ local: buildCursorLocalAgentOptions({ cwd, store: sessionStore.store, settingSources: [] }),
45
+ }));
46
+ throwIfAborted();
47
+ const prompt = buildCursorSummaryPrompt(params.context);
48
+ const promptInputTokens = estimateCursorPromptTokens(prompt, getCursorPromptOptions(params.model));
49
+ const nativeReplayId = createCursorNativeReplayId();
50
+ const textDeltas = [];
51
+ const turnCoordinator = new CursorSdkTurnCoordinator({
52
+ stream: params.stream, partial: params.partial, cwd, resolvedApiKey,
53
+ useNativeToolReplay: false, nativeReplayId, textDeltas, debugRecorder: sdkEventDebug,
54
+ });
55
+ sdkEventDebug?.recordProviderMeta({
56
+ purpose: params.request.purpose,
57
+ runtime: "local",
58
+ summaryAgentId: agent.agentId,
59
+ model: { id: params.model.id, provider: params.model.provider, selection },
60
+ agentMode: "agent", settingSources: [], tools: [], resumedAgent: false,
61
+ });
62
+ completed = true;
63
+ return {
64
+ runtimeTarget: "local", execution: "summary", agent, cwd,
65
+ store: sessionStore.store, storeIdentity: sessionStore.identity,
66
+ payload: { text: prompt.text },
67
+ meta: {
68
+ sendPlan: { mode: "bootstrap", resetAgent: false, reason: "initial" },
69
+ prompt, bootstrap: true, promptInputTokens,
70
+ useNativeToolReplay: false, bridgeEnabled: false, nativeReplayId,
71
+ agentMode: "agent", modelSelection: selection,
72
+ },
73
+ contextWindowAgentId: agent.agentId, textDeltas, restoreCursorSdkOutputFilter,
74
+ lifecycle: summaryLifecycle(agent, sessionStore),
75
+ runtime: { kind: "direct", turnCoordinator },
76
+ };
77
+ }
78
+ finally {
79
+ if (!completed) {
80
+ try {
81
+ await agent?.[Symbol.asyncDispose]().catch(() => { });
82
+ }
83
+ finally {
84
+ await sessionStore?.dispose().catch(() => { });
85
+ restoreCursorSdkOutputFilter?.();
86
+ }
87
+ }
88
+ }
89
+ }
@@ -6,7 +6,6 @@ import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-gu
6
6
  import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
7
7
  import { resolveCursorApiKey } from "./cursor-api-key.js";
8
8
  import { CursorProviderTurnRunner } from "./cursor-provider-turn-runner.js";
9
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
10
9
  import { runExclusiveCursorSessionTurn, __testUtils as cursorSessionTurnQueueTestUtils } from "./cursor-session-turn-queue.js";
11
10
  function makeInitialMessage(model) {
12
11
  return {
@@ -27,7 +26,8 @@ function makeInitialMessage(model) {
27
26
  timestamp: Date.now(),
28
27
  };
29
28
  }
30
- export function streamCursor(model, context, options) {
29
+ export function streamCursor(model, context, options, ownership) {
30
+ const { scope, bridge, nativeDisplay, recordCloudLifecycle, request, usageRecorder } = ownership;
31
31
  const stream = createAssistantMessageEventStream();
32
32
  const sdkEventDebugRef = {};
33
33
  attachCursorSdkEventDebugPiStreamTap(stream, sdkEventDebugRef);
@@ -35,6 +35,12 @@ export function streamCursor(model, context, options) {
35
35
  const partial = makeInitialMessage(model);
36
36
  const runner = new CursorProviderTurnRunner({
37
37
  model,
38
+ scope,
39
+ bridge,
40
+ nativeDisplay,
41
+ recordCloudLifecycle,
42
+ request,
43
+ usageRecorder,
38
44
  context,
39
45
  stream,
40
46
  partial,
@@ -43,7 +49,7 @@ export function streamCursor(model, context, options) {
43
49
  });
44
50
  try {
45
51
  stream.push({ type: "start", partial });
46
- await runExclusiveCursorSessionTurn(getCursorSessionScopeKey(), () => runner.run(installCursorSdkProcessErrorGuard()), options?.signal);
52
+ await runExclusiveCursorSessionTurn(scope.scopeKey, () => runner.run(installCursorSdkProcessErrorGuard()), options?.signal);
47
53
  }
48
54
  catch (error) {
49
55
  await runner.handleOuterCatch(error);
@@ -0,0 +1,42 @@
1
+ import { createHash } from "node:crypto";
2
+ import { convertToLlm } from "@earendil-works/pi-coding-agent";
3
+ function projectionDigest(messages) {
4
+ const hash = createHash("sha256");
5
+ for (const message of messages) {
6
+ // Stable value equivalence survives Pi's cloning and property insertion order.
7
+ hash.update(JSON.stringify(message, (_key, value) => {
8
+ if (!value || typeof value !== "object" || Array.isArray(value))
9
+ return value;
10
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
11
+ }));
12
+ hash.update("\n");
13
+ }
14
+ return hash.digest("hex");
15
+ }
16
+ export function captureCursorRequestProjection(manager) {
17
+ const branch = manager.getBranch();
18
+ const projection = manager.buildSessionProjection();
19
+ const order = new Map(branch.map((entry, index) => [entry.id, index]));
20
+ const boundary = branch.findLastIndex((entry) => entry.type === "compaction" || entry.type === "context_edit");
21
+ const measurements = [];
22
+ // Raw branch chronology, not projected order or clocks, determines freshness.
23
+ for (const entry of [...projection.entries].sort((a, b) => (order.get(a.sourceEntry.id) ?? -1) - (order.get(b.sourceEntry.id) ?? -1))) {
24
+ if ((order.get(entry.sourceEntry.id) ?? -1) <= boundary)
25
+ continue;
26
+ for (const message of entry.messages) {
27
+ if (message.role !== "assistant" || message.stopReason === "error" || message.stopReason === "aborted")
28
+ continue;
29
+ const tokens = message.usage.totalTokens;
30
+ if (!Number.isFinite(tokens) || tokens <= 0)
31
+ continue;
32
+ measurements.push(Object.freeze({ api: message.api, provider: message.provider, model: message.model, tokens }));
33
+ }
34
+ }
35
+ return Object.freeze({ digest: projectionDigest(convertToLlm(projection.messages)), measurements: Object.freeze(measurements) });
36
+ }
37
+ export function resolveCursorRequestProvenance(snapshot, model, context, purpose) {
38
+ if (purpose !== "normal" || snapshot.digest !== projectionDigest(context.messages))
39
+ return Object.freeze({ purpose });
40
+ const measurement = snapshot.measurements.findLast((item) => item.api === model.api && item.provider === model.provider && item.model === model.id && item.tokens <= model.contextWindow);
41
+ return Object.freeze({ purpose, ...(measurement ? { occupancyFloor: measurement.tokens } : {}) });
42
+ }
@@ -2,12 +2,23 @@ import { accessSync, constants } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
4
  const RIPGREP_ENV = "CURSOR_RIPGREP_PATH";
5
- export function resolveBundledCursorRipgrepPath(fromModuleUrl = import.meta.url) {
5
+ export function resolveCursorSdkPlatformPackageDirectory(fromModuleUrl = import.meta.url) {
6
6
  try {
7
7
  const require = createRequire(fromModuleUrl);
8
- const platformPackage = `@cursor/sdk-${process.platform}-${process.arch}`;
9
8
  const sdkEntry = require.resolve("@cursor/sdk");
10
- const packageDirectory = dirname(require.resolve(`${platformPackage}/package.json`, { paths: [dirname(sdkEntry)] }));
9
+ return dirname(require.resolve(`@cursor/sdk-${process.platform}-${process.arch}/package.json`, {
10
+ paths: [dirname(sdkEntry)],
11
+ }));
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ export function resolveBundledCursorRipgrepPath(fromModuleUrl = import.meta.url) {
18
+ const packageDirectory = resolveCursorSdkPlatformPackageDirectory(fromModuleUrl);
19
+ if (!packageDirectory)
20
+ return undefined;
21
+ try {
11
22
  const ripgrepPath = join(packageDirectory, "bin", process.platform === "win32" ? "rg.exe" : "rg");
12
23
  accessSync(ripgrepPath, constants.X_OK);
13
24
  return ripgrepPath;