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,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 type { CursorRuntime } from "./cursor-config.js";
6
5
  import { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
@@ -22,14 +21,15 @@ import type {
22
21
  CursorProviderTurnSendResult,
23
22
  LiveCursorProviderTurnRuntime,
24
23
  LocalCursorProviderTurnPrepareResult,
24
+ StartedCursorProviderTurn,
25
25
  } from "./cursor-provider-turn-types.js";
26
26
 
27
27
  export type { CursorProviderTurnRunnerParams } from "./cursor-provider-turn-types.js";
28
28
 
29
- type LocalLivePreparedTurn = LocalCursorProviderTurnPrepareResult & { runtime: LiveCursorProviderTurnRuntime };
29
+ type LocalLivePreparedTurn = StartedCursorProviderTurn & LocalCursorProviderTurnPrepareResult & { runtime: LiveCursorProviderTurnRuntime };
30
30
 
31
- function requireLocalLivePreparedTurn(prepared: CursorProviderTurnPrepareResult): LocalLivePreparedTurn {
32
- if (prepared.runtimeTarget !== "local" || prepared.runtime.kind !== "live") {
31
+ function requireLocalLivePreparedTurn(prepared: StartedCursorProviderTurn): LocalLivePreparedTurn {
32
+ if (prepared.runtimeTarget !== "local" || prepared.execution !== "conversation" || prepared.runtime.kind !== "live") {
33
33
  throw new Error("Cursor live run requires a local live prepared turn");
34
34
  }
35
35
  return prepared as LocalLivePreparedTurn;
@@ -53,6 +53,7 @@ export class CursorProviderTurnRunner {
53
53
  async run(sdkProcessErrorGuard: ReturnType<typeof installCursorSdkProcessErrorGuard>): Promise<void> {
54
54
  const { stream, partial, model, context, options, sdkEventDebugRef } = this.params;
55
55
  let prepared: CursorProviderTurnPrepareResult | undefined;
56
+ let started: StartedCursorProviderTurn | undefined;
56
57
  let sendResult: CursorProviderTurnSendResult | undefined;
57
58
  let liveCompletion: CursorLiveRunCompletion | undefined;
58
59
  const runFinalizer = new CursorRunFinalizer({
@@ -65,21 +66,23 @@ export class CursorProviderTurnRunner {
65
66
 
66
67
  try {
67
68
  this.throwIfAborted();
68
- const cwd = getCursorSessionCwd();
69
+ const { scope } = this.params;
70
+ const cwd = scope.cwd;
69
71
  this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
70
72
  cwd,
71
73
  modelId: model.id,
72
74
  provider: model.provider,
75
+ scope,
73
76
  });
74
77
  sdkEventDebugRef.current = this.sdkEventDebug;
75
78
  this.sdkEventDebug?.recordContextSnapshot(context);
76
79
  // Resolved once here, before any drain await, so the drain decision and the
77
80
  // prepare dispatch below always act on the same config snapshot.
78
- const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
81
+ const resolvedConfig = resolveCursorProviderTurnConfig(cwd, scope.projectTrusted, scope.scopeKey);
79
82
  this.runtimeTarget = resolvedConfig.runtime.value;
80
- if (resolvedConfig.runtime.value === "local") {
83
+ if (resolvedConfig.runtime.value === "local" && this.params.request.purpose === "normal") {
81
84
  if (
82
- (await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug)) ===
85
+ (await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug, scope.scopeKey, this.params.request.occupancyFloor)) ===
83
86
  "stream_ended"
84
87
  ) {
85
88
  return;
@@ -97,9 +100,33 @@ export class CursorProviderTurnRunner {
97
100
  resolvedConfig,
98
101
  });
99
102
 
103
+ const usage = await this.params.usageRecorder.start({
104
+ agent: prepared.agent,
105
+ runtime: prepared.runtimeTarget,
106
+ model: { id: model.id, provider: model.provider, cost: { ...model.cost } },
107
+ modelSelection: prepared.meta.modelSelection,
108
+ purpose: this.params.request.purpose,
109
+ ...(prepared.runtimeTarget === "local" ? { storeIdentity: prepared.storeIdentity.stateRoot } : {}),
110
+ resumed: prepared.runtimeTarget === "local" && prepared.execution === "conversation" && prepared.sessionAgentLease.resumed === true,
111
+ newlyCreated: prepared.runtimeTarget !== "local" || prepared.execution === "summary" || (prepared.sessionAgentLease.created && !prepared.sessionAgentLease.resumed),
112
+ }).catch(error => {
113
+ this.params.usageRecorder.notePersistenceFailure(error);
114
+ throw error;
115
+ });
116
+ started = { ...prepared, usage };
117
+ if (started.runtime.liveRun) {
118
+ started.runtime.liveRun.onAbandon = async () => {
119
+ try {
120
+ await usage.recordTerminal({ status: "abandon" });
121
+ } catch (error) {
122
+ usage.notePersistenceFailure(error);
123
+ }
124
+ };
125
+ }
126
+
100
127
  sendResult = await sendCursorProviderTurn({
101
128
  params: this.params,
102
- prepared,
129
+ prepared: started,
103
130
  sdkEventDebug: this.sdkEventDebug,
104
131
  sdkProcessErrorGuard,
105
132
  throwIfAborted: () => this.throwIfAborted(),
@@ -108,7 +135,7 @@ export class CursorProviderTurnRunner {
108
135
  const { send } = sendResult;
109
136
 
110
137
  if (prepared.runtime.kind === "live") {
111
- const livePrepared = requireLocalLivePreparedTurn(prepared);
138
+ const livePrepared = requireLocalLivePreparedTurn(started);
112
139
  liveCompletion = runFinalizer.startLiveRunCompletion({
113
140
  send,
114
141
  prepared: livePrepared,
@@ -126,7 +153,7 @@ export class CursorProviderTurnRunner {
126
153
 
127
154
  const outcomePromise = awaitFinalizeCursorRunOutcome({
128
155
  run: send.run,
129
- prepared,
156
+ prepared: started,
130
157
  cursorAgentMessageOffset: send.cursorAgentMessageOffset,
131
158
  modelId: model.id,
132
159
  signal: options?.signal,
@@ -141,12 +168,12 @@ export class CursorProviderTurnRunner {
141
168
  const finalized = await outcomePromise;
142
169
  await runFinalizer.applyTerminalEvent({
143
170
  kind: "direct",
144
- prepared,
171
+ prepared: started,
145
172
  outcome: finalized.outcome,
146
173
  displayOnlyTraceBlock: finalized.displayOnlyTraceBlock,
147
174
  });
148
175
  } catch (error) {
149
- await runFinalizer.applyTerminalEvent({ kind: "error", prepared, error });
176
+ await runFinalizer.applyTerminalEvent({ kind: "error", prepared: started ?? prepared, error });
150
177
  } finally {
151
178
  await runFinalizer.cleanup(prepared, sendResult, liveCompletion);
152
179
  }
@@ -1,24 +1,20 @@
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";
7
- import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
3
+ import { createCursorCloudLifecyclePersistenceError } from "./cursor-cloud-lifecycle.js";
8
4
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
9
5
  import { consumeCursorLocalForceOverride } from "./cursor-runtime-state.js";
10
6
  import { recordCursorSessionAgentLineage } from "./cursor-session-agent-lineage.js";
11
7
  import type { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
12
8
  import type {
13
9
  CursorProviderTurnRunnerParams,
14
- CursorProviderTurnPrepareResult,
10
+ StartedCursorProviderTurn,
15
11
  CursorProviderTurnSendResult,
16
12
  } from "./cursor-provider-turn-types.js";
17
13
  import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
18
14
 
19
15
  export interface SendCursorProviderTurnParams {
20
16
  params: CursorProviderTurnRunnerParams;
21
- prepared: CursorProviderTurnPrepareResult;
17
+ prepared: StartedCursorProviderTurn;
22
18
  sdkEventDebug: CursorSdkEventDebugSink | undefined;
23
19
  sdkProcessErrorGuard: ReturnType<typeof installCursorSdkProcessErrorGuard>;
24
20
  throwIfAborted: () => void;
@@ -27,7 +23,7 @@ export interface SendCursorProviderTurnParams {
27
23
 
28
24
  const CLOUD_LEDGER_CANCEL_TIMEOUT_MS = 5000;
29
25
 
30
- async function requestBoundedCloudRunCancellation(run: Awaited<ReturnType<CursorProviderTurnPrepareResult["agent"]["send"]>>): Promise<boolean> {
26
+ async function requestBoundedCloudRunCancellation(run: Awaited<ReturnType<StartedCursorProviderTurn["agent"]["send"]>>): Promise<boolean> {
31
27
  let timer: ReturnType<typeof setTimeout> | undefined;
32
28
  const timeout = new Promise<false>((resolve) => {
33
29
  timer = setTimeout(() => resolve(false), CLOUD_LEDGER_CANCEL_TIMEOUT_MS);
@@ -75,9 +71,9 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
75
71
  abortRegistration?.signal.addEventListener("abort", abortListener, { once: true });
76
72
  throwIfAborted();
77
73
  let cursorAgentMessageOffset: number | undefined;
78
- if (prepared.runtimeTarget === "local") {
74
+ if (prepared.runtimeTarget === "local" && prepared.execution === "conversation") {
79
75
  try {
80
- cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd, prepared.sessionAgentLease.store);
76
+ cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd, prepared.store);
81
77
  } catch (error) {
82
78
  recordDebug(() => sdkEventDebug?.recordError("cursor_agent_message_count", error));
83
79
  }
@@ -102,6 +98,13 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
102
98
  mode: meta.agentMode,
103
99
  model: meta.modelSelection,
104
100
  onDelta: (args) => {
101
+ if (args.update.type === "turn-ended") {
102
+ try {
103
+ prepared.usage.observeRawTurn(args.update.usage);
104
+ } catch (error) {
105
+ params.usageRecorder.notePersistenceFailure(error);
106
+ }
107
+ }
105
108
  recordDebug(() => sdkEventDebug?.recordOnDelta(args.update));
106
109
  turnCoordinator.handleDelta(args.update);
107
110
  },
@@ -111,17 +114,23 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
111
114
  },
112
115
  };
113
116
  throwIfAborted();
114
- if (prepared.runtimeTarget === "local" && consumeCursorLocalForceOverride(prepared.localForce)) {
117
+ if (prepared.runtimeTarget === "local" && prepared.execution === "conversation" && consumeCursorLocalForceOverride(prepared.localForce, params.scope.scopeKey)) {
115
118
  sendOptions.local = { force: true };
116
119
  }
117
120
  const runPromise = agent.send(payload, sendOptions);
118
121
  // Record at send initiation (promise created), including later reject/cancel paths.
119
- if (prepared.runtimeTarget === "local") {
120
- recordCursorSessionAgentLineage(agent.agentId);
122
+ if (prepared.runtimeTarget === "local" && prepared.execution === "conversation") {
123
+ recordCursorSessionAgentLineage(agent.agentId, params.scope.scopeKey);
121
124
  }
122
125
  const run = await runPromise;
123
126
  sdkRun = run;
124
- if (prepared.runtimeTarget === "cloud" && !recordCursorCloudLifecycleSafely({ agentId: run.agentId, runId: run.id }, resolvedApiKey)) {
127
+ runtime.sdkRun = run;
128
+ try {
129
+ await prepared.usage.recordRun({ runId: run.id, requestId: run.requestId });
130
+ } catch (error) {
131
+ params.usageRecorder.notePersistenceFailure(error);
132
+ }
133
+ if (prepared.runtimeTarget === "cloud" && !params.recordCloudLifecycle({ agentId: run.agentId, runId: run.id }, resolvedApiKey)) {
125
134
  const cancellationConfirmed = await requestBoundedCloudRunCancellation(run);
126
135
  throw createCursorCloudLifecyclePersistenceError(run.agentId, "run", cancellationConfirmed, resolvedApiKey);
127
136
  }
@@ -143,7 +152,6 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
143
152
  sdkProcessErrorGuard.suppressAbortErrors();
144
153
  liveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
145
154
  await run.cancel().catch(() => {});
146
- throw new CursorLiveRunAbortError();
147
155
  }
148
156
 
149
157
  completed = true;
@@ -0,0 +1,94 @@
1
+ import type { ModelSelection, SDKAgent } from "@cursor/sdk";
2
+ import { buildCursorSummaryPrompt, estimateCursorPromptTokens } from "./context.js";
3
+ import { configureCursorSdkHttp1 } from "./cursor-http1.js";
4
+ import { createCursorNativeReplayId } from "./cursor-provider-live-run-drain.js";
5
+ import type { PrepareCursorProviderTurnParams } from "./cursor-provider-turn-prepare.js";
6
+ import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
7
+ import type { CursorProviderTurnLifecycle, SummaryCursorProviderTurnPrepareResult } from "./cursor-provider-turn-types.js";
8
+ import { ensureCursorRipgrepPath } from "./cursor-ripgrep-path.js";
9
+ import { loadCursorSdk } from "./cursor-sdk-runtime.js";
10
+ import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
11
+ import { buildCursorLocalAgentOptions } from "./cursor-session-agent.js";
12
+ import { openCursorSessionStoreForScope, type OpenCursorSessionStore } from "./cursor-session-store.js";
13
+ import { getCursorPromptOptions } from "./cursor-usage-accounting.js";
14
+
15
+ function summaryLifecycle(agent: SDKAgent, sessionStore: OpenCursorSessionStore): CursorProviderTurnLifecycle {
16
+ let disposal: Promise<void> | undefined;
17
+ const dispose = () => disposal ??= (async () => {
18
+ try {
19
+ await agent[Symbol.asyncDispose]();
20
+ } finally {
21
+ await sessionStore.dispose();
22
+ }
23
+ })();
24
+ return { trackRunCompletion: () => {}, commitSend: () => {}, abandon: dispose, dispose };
25
+ }
26
+
27
+ /** A native summary never enters the conversation pool or inherits its tool/mode/resume state. */
28
+ export async function prepareCursorSummaryProviderTurn(
29
+ prepareParams: PrepareCursorProviderTurnParams,
30
+ selection: ModelSelection,
31
+ ): Promise<SummaryCursorProviderTurnPrepareResult> {
32
+ const { params, cwd, resolvedApiKey, resolvedConfig, sdkEventDebug, throwIfAborted } = prepareParams;
33
+ let sessionStore: OpenCursorSessionStore | undefined;
34
+ let agent: SDKAgent | undefined;
35
+ let restoreCursorSdkOutputFilter: (() => void) | undefined;
36
+ let completed = false;
37
+ try {
38
+ ensureCursorRipgrepPath();
39
+ const sdk = await loadCursorSdk();
40
+ configureCursorSdkHttp1(sdk, resolvedConfig.local.useHttp1ForAgent);
41
+ restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
42
+ sessionStore = (await openCursorSessionStoreForScope({
43
+ cwd, scopeKey: params.scope.scopeKey, persistent: false, hasResumeHandle: false,
44
+ })).sessionStore;
45
+ throwIfAborted();
46
+ agent = await suppressCursorSdkOutput(() => sdk.Agent.create({
47
+ apiKey: resolvedApiKey,
48
+ model: selection,
49
+ mode: "agent",
50
+ tools: [],
51
+ local: buildCursorLocalAgentOptions({ cwd, store: sessionStore!.store, settingSources: [] }),
52
+ }));
53
+ throwIfAborted();
54
+ const prompt = buildCursorSummaryPrompt(params.context);
55
+ const promptInputTokens = estimateCursorPromptTokens(prompt, getCursorPromptOptions(params.model));
56
+ const nativeReplayId = createCursorNativeReplayId();
57
+ const textDeltas: string[] = [];
58
+ const turnCoordinator = new CursorSdkTurnCoordinator({
59
+ stream: params.stream, partial: params.partial, cwd, resolvedApiKey,
60
+ useNativeToolReplay: false, nativeReplayId, textDeltas, debugRecorder: sdkEventDebug,
61
+ });
62
+ sdkEventDebug?.recordProviderMeta({
63
+ purpose: params.request.purpose,
64
+ runtime: "local",
65
+ summaryAgentId: agent.agentId,
66
+ model: { id: params.model.id, provider: params.model.provider, selection },
67
+ agentMode: "agent", settingSources: [], tools: [], resumedAgent: false,
68
+ });
69
+ completed = true;
70
+ return {
71
+ runtimeTarget: "local", execution: "summary", agent, cwd,
72
+ store: sessionStore.store, storeIdentity: sessionStore.identity,
73
+ payload: { text: prompt.text },
74
+ meta: {
75
+ sendPlan: { mode: "bootstrap", resetAgent: false, reason: "initial" },
76
+ prompt, bootstrap: true, promptInputTokens,
77
+ useNativeToolReplay: false, bridgeEnabled: false, nativeReplayId,
78
+ agentMode: "agent", modelSelection: selection,
79
+ },
80
+ contextWindowAgentId: agent.agentId, textDeltas, restoreCursorSdkOutputFilter,
81
+ lifecycle: summaryLifecycle(agent, sessionStore),
82
+ runtime: { kind: "direct", turnCoordinator },
83
+ };
84
+ } finally {
85
+ if (!completed) {
86
+ try {
87
+ await agent?.[Symbol.asyncDispose]().catch(() => {});
88
+ } finally {
89
+ await sessionStore?.dispose().catch(() => {});
90
+ restoreCursorSdkOutputFilter?.();
91
+ }
92
+ }
93
+ }
94
+ }
@@ -6,7 +6,7 @@ import type {
6
6
  Model,
7
7
  SimpleStreamOptions,
8
8
  } from "@earendil-works/pi-ai";
9
- import type { AgentModeOption, ModelSelection, SDKAgent, SDKImage } from "@cursor/sdk";
9
+ import type { AgentModeOption, LocalAgentStore, 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";
@@ -14,9 +14,22 @@ 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
16
  import type { CursorResolvedSetting } from "./cursor-config.js";
17
- import type { CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
17
+ import type { CursorRequestProvenance } from "./cursor-request-provenance.js";
18
+ import type { CursorUsageRecorder, CursorUsageTurnRecorder } from "./cursor-usage-ledger.js";
19
+ import type { CursorSessionStoreIdentity } from "./cursor-session-store.js";
20
+
21
+ import type { CursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
22
+ import type { CursorCloudLifecycleRecorder } from "./cursor-cloud-lifecycle.js";
23
+ import type { CursorNativeToolDisplayState } from "./cursor-native-tool-display-state.js";
24
+ import type { CursorTurnScope } from "./cursor-session-scope.js";
18
25
 
19
26
  export interface CursorProviderTurnRunnerParams {
27
+ scope: CursorTurnScope;
28
+ bridge?: CursorPiToolBridge;
29
+ nativeDisplay?: CursorNativeToolDisplayState;
30
+ recordCloudLifecycle: CursorCloudLifecycleRecorder;
31
+ request: CursorRequestProvenance;
32
+ usageRecorder: CursorUsageRecorder;
20
33
  model: Model<Api>;
21
34
  context: Context;
22
35
  stream: AssistantMessageEventStream;
@@ -45,7 +58,8 @@ export interface CursorProviderTurnSendMeta {
45
58
 
46
59
  interface CursorProviderTurnRuntimeBase {
47
60
  turnCoordinator: CursorSdkTurnCoordinator;
48
- billedTurnUsage?: CursorSdkTurnUsage;
61
+ sdkRun?: Awaited<ReturnType<SDKAgent["send"]>>;
62
+ usageTerminalRecorded?: boolean;
49
63
  }
50
64
 
51
65
  /**
@@ -85,15 +99,30 @@ interface CursorProviderTurnPrepareResultBase {
85
99
  lifecycle: CursorProviderTurnLifecycle;
86
100
  }
87
101
 
88
- export interface LocalCursorProviderTurnPrepareResult extends CursorProviderTurnPrepareResultBase {
102
+ interface LocalCursorProviderTurnPrepareResultBase extends CursorProviderTurnPrepareResultBase {
89
103
  runtimeTarget: "local";
104
+ store: LocalAgentStore;
105
+ storeIdentity: CursorSessionStoreIdentity;
106
+ }
107
+
108
+ export interface LocalCursorProviderTurnPrepareResult extends LocalCursorProviderTurnPrepareResultBase {
109
+ execution: "conversation";
90
110
  runtime: CursorProviderTurnRuntime;
91
111
  sessionAgentScopeKey: string;
92
112
  sessionAgentLease: SessionCursorAgentLease;
93
113
  localForce: CursorResolvedSetting<boolean>;
94
114
  }
95
115
 
116
+ export interface SummaryCursorProviderTurnPrepareResult extends LocalCursorProviderTurnPrepareResultBase {
117
+ execution: "summary";
118
+ runtime: DirectCursorProviderTurnRuntime;
119
+ sessionAgentScopeKey?: undefined;
120
+ sessionAgentLease?: undefined;
121
+ }
122
+
96
123
  export interface CloudCursorProviderTurnPrepareResult extends CursorProviderTurnPrepareResultBase {
124
+ execution: "conversation";
125
+ recordCloudLifecycle: CursorCloudLifecycleRecorder;
97
126
  runtimeTarget: "cloud";
98
127
  runtime: DirectCursorProviderTurnRuntime;
99
128
  sessionAgentScopeKey?: undefined;
@@ -108,8 +137,12 @@ export interface CloudCursorProviderTurnPrepareResult extends CursorProviderTurn
108
137
  */
109
138
  export type CursorProviderTurnPrepareResult =
110
139
  | LocalCursorProviderTurnPrepareResult
140
+ | SummaryCursorProviderTurnPrepareResult
111
141
  | CloudCursorProviderTurnPrepareResult;
112
142
 
143
+ /** Accounting has claimed the origin before this turn can execute a send. */
144
+ export type StartedCursorProviderTurn = CursorProviderTurnPrepareResult & { usage: CursorUsageTurnRecorder };
145
+
113
146
  export interface CursorProviderTurnSend {
114
147
  run: Awaited<ReturnType<SDKAgent["send"]>>;
115
148
  cursorAgentMessageOffset: number | undefined;
@@ -22,7 +22,12 @@ import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-gu
22
22
  import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
23
23
  import { resolveCursorApiKey } from "./cursor-api-key.js";
24
24
  import { CursorProviderTurnRunner } from "./cursor-provider-turn-runner.js";
25
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
25
+ import type { CursorTurnScope } from "./cursor-session-scope.js";
26
+ import type { CursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
27
+ import type { CursorNativeToolDisplayState } from "./cursor-native-tool-display-state.js";
28
+ import type { CursorCloudLifecycleRecorder } from "./cursor-cloud-lifecycle.js";
29
+ import type { CursorRequestProvenance } from "./cursor-request-provenance.js";
30
+ import type { CursorUsageRecorder } from "./cursor-usage-ledger.js";
26
31
  import { runExclusiveCursorSessionTurn, __testUtils as cursorSessionTurnQueueTestUtils } from "./cursor-session-turn-queue.js";
27
32
 
28
33
  function makeInitialMessage(model: Model<Api>): AssistantMessage {
@@ -45,11 +50,22 @@ function makeInitialMessage(model: Model<Api>): AssistantMessage {
45
50
  };
46
51
  }
47
52
 
53
+ export interface CursorProviderOwnership {
54
+ scope: CursorTurnScope;
55
+ bridge?: CursorPiToolBridge;
56
+ nativeDisplay?: CursorNativeToolDisplayState;
57
+ recordCloudLifecycle: CursorCloudLifecycleRecorder;
58
+ request: CursorRequestProvenance;
59
+ usageRecorder: CursorUsageRecorder;
60
+ }
61
+
48
62
  export function streamCursor(
49
63
  model: Model<Api>,
50
64
  context: Context,
51
- options?: SimpleStreamOptions,
65
+ options: SimpleStreamOptions | undefined,
66
+ ownership: CursorProviderOwnership,
52
67
  ): AssistantMessageEventStream {
68
+ const { scope, bridge, nativeDisplay, recordCloudLifecycle, request, usageRecorder } = ownership;
53
69
  const stream = createAssistantMessageEventStream();
54
70
  const sdkEventDebugRef: { current?: CursorSdkEventDebugSink } = {};
55
71
  attachCursorSdkEventDebugPiStreamTap(stream, sdkEventDebugRef);
@@ -59,6 +75,12 @@ export function streamCursor(
59
75
 
60
76
  const runner = new CursorProviderTurnRunner({
61
77
  model,
78
+ scope,
79
+ bridge,
80
+ nativeDisplay,
81
+ recordCloudLifecycle,
82
+ request,
83
+ usageRecorder,
62
84
  context,
63
85
  stream,
64
86
  partial,
@@ -69,7 +91,7 @@ export function streamCursor(
69
91
  try {
70
92
  stream.push({ type: "start", partial });
71
93
  await runExclusiveCursorSessionTurn(
72
- getCursorSessionScopeKey(),
94
+ scope.scopeKey,
73
95
  () => runner.run(installCursorSdkProcessErrorGuard()),
74
96
  options?.signal,
75
97
  );
@@ -0,0 +1,64 @@
1
+ import { createHash } from "node:crypto";
2
+ import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { Api, Context, Model } from "@earendil-works/pi-ai";
4
+
5
+ export interface CursorRequestProvenance {
6
+ readonly purpose: "normal" | "compaction" | "tree";
7
+ readonly occupancyFloor?: number;
8
+ }
9
+
10
+ interface Measurement {
11
+ readonly api: string;
12
+ readonly provider: string;
13
+ readonly model: string;
14
+ readonly tokens: number;
15
+ }
16
+
17
+ /** Receipt data contains hashes and scalars only, never conversation text. */
18
+ export interface CursorRequestProjectionSnapshot {
19
+ readonly digest: string;
20
+ readonly measurements: readonly Measurement[];
21
+ }
22
+
23
+ function projectionDigest(messages: readonly unknown[]): string {
24
+ const hash = createHash("sha256");
25
+ for (const message of messages) {
26
+ // Stable value equivalence survives Pi's cloning and property insertion order.
27
+ hash.update(JSON.stringify(message, (_key, value: unknown) => {
28
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
29
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
30
+ }));
31
+ hash.update("\n");
32
+ }
33
+ return hash.digest("hex");
34
+ }
35
+
36
+ export function captureCursorRequestProjection(manager: ExtensionContext["sessionManager"]): CursorRequestProjectionSnapshot {
37
+ const branch = manager.getBranch();
38
+ const projection = manager.buildSessionProjection();
39
+ const order = new Map(branch.map((entry, index) => [entry.id, index]));
40
+ const boundary = branch.findLastIndex((entry) => entry.type === "compaction" || entry.type === "context_edit");
41
+ const measurements: Measurement[] = [];
42
+ // Raw branch chronology, not projected order or clocks, determines freshness.
43
+ for (const entry of [...projection.entries].sort((a, b) => (order.get(a.sourceEntry.id) ?? -1) - (order.get(b.sourceEntry.id) ?? -1))) {
44
+ if ((order.get(entry.sourceEntry.id) ?? -1) <= boundary) continue;
45
+ for (const message of entry.messages) {
46
+ if (message.role !== "assistant" || message.stopReason === "error" || message.stopReason === "aborted") continue;
47
+ const tokens = message.usage.totalTokens;
48
+ if (!Number.isFinite(tokens) || tokens <= 0) continue;
49
+ measurements.push(Object.freeze({ api: message.api, provider: message.provider, model: message.model, tokens }));
50
+ }
51
+ }
52
+ return Object.freeze({ digest: projectionDigest(convertToLlm(projection.messages)), measurements: Object.freeze(measurements) });
53
+ }
54
+
55
+ export function resolveCursorRequestProvenance(
56
+ snapshot: CursorRequestProjectionSnapshot,
57
+ model: Model<Api>,
58
+ context: Context,
59
+ purpose: CursorRequestProvenance["purpose"],
60
+ ): CursorRequestProvenance {
61
+ if (purpose !== "normal" || snapshot.digest !== projectionDigest(context.messages)) return Object.freeze({ purpose });
62
+ const measurement = snapshot.measurements.findLast((item) => item.api === model.api && item.provider === model.provider && item.model === model.id && item.tokens <= model.contextWindow);
63
+ return Object.freeze({ purpose, ...(measurement ? { occupancyFloor: measurement.tokens } : {}) });
64
+ }
@@ -4,16 +4,26 @@ import { dirname, isAbsolute, join } from "node:path";
4
4
 
5
5
  const RIPGREP_ENV = "CURSOR_RIPGREP_PATH";
6
6
 
7
- export function resolveBundledCursorRipgrepPath(
7
+ export function resolveCursorSdkPlatformPackageDirectory(
8
8
  fromModuleUrl: string | URL = import.meta.url,
9
9
  ): string | undefined {
10
10
  try {
11
11
  const require = createRequire(fromModuleUrl);
12
- const platformPackage = `@cursor/sdk-${process.platform}-${process.arch}`;
13
12
  const sdkEntry = require.resolve("@cursor/sdk");
14
- const packageDirectory = dirname(
15
- require.resolve(`${platformPackage}/package.json`, { paths: [dirname(sdkEntry)] }),
16
- );
13
+ return dirname(require.resolve(`@cursor/sdk-${process.platform}-${process.arch}/package.json`, {
14
+ paths: [dirname(sdkEntry)],
15
+ }));
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ export function resolveBundledCursorRipgrepPath(
22
+ fromModuleUrl: string | URL = import.meta.url,
23
+ ): string | undefined {
24
+ const packageDirectory = resolveCursorSdkPlatformPackageDirectory(fromModuleUrl);
25
+ if (!packageDirectory) return undefined;
26
+ try {
17
27
  const ripgrepPath = join(packageDirectory, "bin", process.platform === "win32" ? "rg.exe" : "rg");
18
28
  accessSync(ripgrepPath, constants.X_OK);
19
29
  return ripgrepPath;