pi-cursor-sdk 0.1.18 → 0.1.20
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.
- package/CHANGELOG.md +58 -0
- package/README.md +59 -1
- package/docs/cursor-live-smoke-checklist.md +4 -1
- package/docs/cursor-model-ux-spec.md +7 -5
- package/docs/cursor-native-tool-replay.md +99 -3
- package/docs/cursor-testing-lessons.md +234 -5
- package/package.json +10 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/probe-mcp-coldstart.mjs +244 -0
- package/scripts/validate-smoke-jsonl.mjs +27 -3
- package/src/context.ts +45 -32
- package/src/cursor-agent-message-web-tools.ts +172 -0
- package/src/cursor-agents-context.ts +176 -0
- package/src/cursor-incomplete-tool-visibility.ts +124 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-tool-display-registration.ts +1 -4
- package/src/cursor-native-tool-display-replay.ts +65 -6
- package/src/cursor-native-tool-display-tools.ts +20 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
- package/src/cursor-pi-tool-bridge-run.ts +16 -1
- package/src/cursor-pi-tool-bridge-types.ts +3 -0
- package/src/cursor-provider-errors.ts +96 -0
- package/src/cursor-provider-live-run-drain.ts +181 -62
- package/src/cursor-provider-turn-coordinator.ts +220 -33
- package/src/cursor-provider.ts +302 -93
- package/src/cursor-question-tool.ts +1 -4
- package/src/cursor-sdk-abort-error-guard.ts +109 -0
- package/src/cursor-sdk-event-debug-constants.ts +40 -0
- package/src/cursor-sdk-event-debug-session.ts +163 -0
- package/src/cursor-sdk-event-debug.ts +602 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +279 -82
- package/src/cursor-session-send-policy.ts +43 -0
- package/src/cursor-setting-sources.ts +29 -0
- package/src/cursor-state.ts +1 -5
- package/src/cursor-tool-lifecycle.ts +85 -0
- package/src/cursor-tool-names.ts +39 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-tool-visibility.ts +63 -0
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +135 -24
- package/src/cursor-transcript-utils.ts +12 -0
- package/src/cursor-web-tool-activity.ts +84 -0
- package/src/index.ts +4 -1
|
@@ -16,30 +16,63 @@ import {
|
|
|
16
16
|
} from "./cursor-pi-tool-bridge.js";
|
|
17
17
|
import { computeCursorContextFingerprint } from "./context.js";
|
|
18
18
|
import { getCursorSessionScopeKey, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
|
|
19
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
19
20
|
|
|
20
21
|
export interface SessionCursorAgentSendState {
|
|
21
22
|
bootstrapped: boolean;
|
|
22
23
|
contextFingerprint: string;
|
|
24
|
+
incrementalSendCount: number;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface SessionCursorAgentLease {
|
|
26
28
|
scopeKey: string;
|
|
29
|
+
poolKey: string;
|
|
30
|
+
instanceId: number;
|
|
27
31
|
agent: SDKAgent;
|
|
28
32
|
bridgeRun?: CursorPiToolBridgeRun;
|
|
29
33
|
sendState: SessionCursorAgentSendState;
|
|
30
34
|
created: boolean;
|
|
35
|
+
commitSend(context: Context, bootstrapped: boolean): void;
|
|
36
|
+
trackRunCompletion(completion: Promise<unknown>): void;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
interface
|
|
39
|
+
interface SessionCursorAgentPoolEntryBase {
|
|
34
40
|
poolKey: string;
|
|
41
|
+
instanceId: number;
|
|
35
42
|
scopeKey: string;
|
|
36
|
-
agent?: SDKAgent;
|
|
37
|
-
bridgeRun?: CursorPiToolBridgeRun;
|
|
38
43
|
sendState: SessionCursorAgentSendState;
|
|
39
|
-
creating?: Promise<SessionCursorAgentPoolEntry>;
|
|
40
|
-
creationGeneration?: number;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
interface SessionCursorAgentCreatingEntry extends SessionCursorAgentPoolEntryBase {
|
|
47
|
+
status: "creating";
|
|
48
|
+
creating: Promise<SessionCursorAgentReadyEntry>;
|
|
49
|
+
creationGeneration: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SessionCursorAgentReadyEntry extends SessionCursorAgentPoolEntryBase {
|
|
53
|
+
status: "ready";
|
|
54
|
+
agent: SDKAgent;
|
|
55
|
+
bridgeRun?: CursorPiToolBridgeRun;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SessionCursorAgentBusyEntry extends SessionCursorAgentPoolEntryBase {
|
|
59
|
+
status: "busy";
|
|
60
|
+
agent: SDKAgent;
|
|
61
|
+
bridgeRun?: CursorPiToolBridgeRun;
|
|
62
|
+
completionSettled: Promise<void>;
|
|
63
|
+
pendingCompletion: Promise<void>;
|
|
64
|
+
releaseBusyWait: () => void;
|
|
65
|
+
busyGeneration: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type SessionCursorAgentActiveEntry = SessionCursorAgentReadyEntry | SessionCursorAgentBusyEntry;
|
|
69
|
+
type SessionCursorAgentPoolEntry =
|
|
70
|
+
| SessionCursorAgentCreatingEntry
|
|
71
|
+
| SessionCursorAgentReadyEntry
|
|
72
|
+
| SessionCursorAgentBusyEntry;
|
|
73
|
+
|
|
74
|
+
type SessionCursorAgentPoolState = { status: "empty" } | SessionCursorAgentPoolEntry;
|
|
75
|
+
|
|
43
76
|
class SessionCursorAgentCreationSupersededError extends Error {
|
|
44
77
|
constructor() {
|
|
45
78
|
super("Cursor session agent creation was superseded");
|
|
@@ -74,6 +107,7 @@ interface SessionCursorAgentCreateParams {
|
|
|
74
107
|
modelSelection: ModelSelection;
|
|
75
108
|
settingSources?: SettingSource[];
|
|
76
109
|
onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
110
|
+
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
77
111
|
createAgent?: typeof Agent.create;
|
|
78
112
|
}
|
|
79
113
|
|
|
@@ -89,6 +123,20 @@ const sessionAgentsByScope = new Map<string, SessionCursorAgentPoolEntry>();
|
|
|
89
123
|
const invalidatedScopeKeys = new Set<string>();
|
|
90
124
|
const terminalDisposedScopeKeys = new Set<string>();
|
|
91
125
|
const scopeCreationGenerations = new Map<string, number>();
|
|
126
|
+
const EMPTY_POOL_STATE: SessionCursorAgentPoolState = { status: "empty" };
|
|
127
|
+
let nextSessionAgentInstanceId = 1;
|
|
128
|
+
|
|
129
|
+
function allocateSessionAgentInstanceId(): number {
|
|
130
|
+
return nextSessionAgentInstanceId++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getSessionCursorAgentPoolState(scopeKey: string): SessionCursorAgentPoolState {
|
|
134
|
+
return sessionAgentsByScope.get(scopeKey) ?? EMPTY_POOL_STATE;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isActivePoolEntry(entry: SessionCursorAgentPoolEntry | undefined): entry is SessionCursorAgentActiveEntry {
|
|
138
|
+
return entry?.status === "ready" || entry?.status === "busy";
|
|
139
|
+
}
|
|
92
140
|
|
|
93
141
|
function getScopeCreationGeneration(scopeKey: string): number {
|
|
94
142
|
return scopeCreationGenerations.get(scopeKey) ?? 0;
|
|
@@ -128,13 +176,13 @@ function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCr
|
|
|
128
176
|
}
|
|
129
177
|
|
|
130
178
|
async function disposePoolEntry(entry: SessionCursorAgentPoolEntry): Promise<void> {
|
|
179
|
+
if (!isActivePoolEntry(entry)) return;
|
|
131
180
|
entry.bridgeRun?.cancel("Cursor session agent disposed");
|
|
132
181
|
try {
|
|
133
182
|
await entry.bridgeRun?.dispose();
|
|
134
183
|
} catch {
|
|
135
184
|
// disposal failure should not block session replacement
|
|
136
185
|
}
|
|
137
|
-
if (!entry.agent) return;
|
|
138
186
|
try {
|
|
139
187
|
await entry.agent[Symbol.asyncDispose]();
|
|
140
188
|
} catch {
|
|
@@ -151,47 +199,175 @@ async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?:
|
|
|
151
199
|
invalidatedScopeKeys.delete(scopeKey);
|
|
152
200
|
if (!entry) return;
|
|
153
201
|
sessionAgentsByScope.delete(scopeKey);
|
|
154
|
-
if (entry.
|
|
202
|
+
if (entry.status === "busy") {
|
|
203
|
+
entry.releaseBusyWait();
|
|
204
|
+
}
|
|
205
|
+
if (entry.status === "creating") {
|
|
206
|
+
entry.creating.catch(() => {
|
|
207
|
+
// In-flight Agent.create was orphaned by scope disposal; active waiters surface errors elsewhere.
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
155
211
|
await disposePoolEntry(entry);
|
|
156
212
|
}
|
|
157
213
|
|
|
158
214
|
function createInitialSendState(): SessionCursorAgentSendState {
|
|
159
|
-
return { bootstrapped: false, contextFingerprint: "" };
|
|
215
|
+
return { bootstrapped: false, contextFingerprint: "", incrementalSendCount: 0 };
|
|
160
216
|
}
|
|
161
217
|
|
|
162
218
|
function bindBridgeToolRequest(
|
|
163
|
-
entry:
|
|
219
|
+
entry: SessionCursorAgentActiveEntry,
|
|
164
220
|
onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void,
|
|
165
221
|
): void {
|
|
166
222
|
entry.bridgeRun?.setOnToolRequest(onBridgeToolRequest);
|
|
167
223
|
}
|
|
168
224
|
|
|
225
|
+
function commitSessionAgentSendForLease(
|
|
226
|
+
scopeKey: string,
|
|
227
|
+
poolKey: string,
|
|
228
|
+
instanceId: number,
|
|
229
|
+
context: Context,
|
|
230
|
+
bootstrapped: boolean,
|
|
231
|
+
): void {
|
|
232
|
+
const entry = sessionAgentsByScope.get(scopeKey);
|
|
233
|
+
if (!isActivePoolEntry(entry)) return;
|
|
234
|
+
if (entry.poolKey !== poolKey || entry.instanceId !== instanceId) return;
|
|
235
|
+
entry.sendState.bootstrapped = bootstrapped || entry.sendState.bootstrapped;
|
|
236
|
+
entry.sendState.contextFingerprint = computeCursorContextFingerprint(context);
|
|
237
|
+
if (bootstrapped) {
|
|
238
|
+
entry.sendState.incrementalSendCount = 0;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
entry.sendState.incrementalSendCount += 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeRunCompletion(completion: Promise<unknown>): Promise<void> {
|
|
245
|
+
return Promise.resolve(completion).then(
|
|
246
|
+
() => undefined,
|
|
247
|
+
() => undefined,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildBusyPoolEntry(
|
|
252
|
+
entry: SessionCursorAgentActiveEntry,
|
|
253
|
+
completionSettled: Promise<void>,
|
|
254
|
+
): SessionCursorAgentBusyEntry {
|
|
255
|
+
let releaseBusyWait = (): void => {};
|
|
256
|
+
const releaseSignal = new Promise<"released">((resolve) => {
|
|
257
|
+
releaseBusyWait = () => resolve("released");
|
|
258
|
+
});
|
|
259
|
+
const pendingCompletion = Promise.race([
|
|
260
|
+
completionSettled.then(() => "completed" as const),
|
|
261
|
+
releaseSignal,
|
|
262
|
+
]).then((outcome) => {
|
|
263
|
+
const current = sessionAgentsByScope.get(entry.scopeKey);
|
|
264
|
+
if (
|
|
265
|
+
outcome === "completed" &&
|
|
266
|
+
current?.status === "busy" &&
|
|
267
|
+
current.poolKey === entry.poolKey &&
|
|
268
|
+
current.instanceId === entry.instanceId &&
|
|
269
|
+
current.pendingCompletion === pendingCompletion
|
|
270
|
+
) {
|
|
271
|
+
sessionAgentsByScope.set(entry.scopeKey, { ...current, status: "ready" });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
...entry,
|
|
277
|
+
status: "busy",
|
|
278
|
+
completionSettled,
|
|
279
|
+
pendingCompletion,
|
|
280
|
+
releaseBusyWait,
|
|
281
|
+
busyGeneration: getScopeCreationGeneration(entry.scopeKey),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function trackSessionAgentRunCompletionForLease(
|
|
286
|
+
scopeKey: string,
|
|
287
|
+
poolKey: string,
|
|
288
|
+
instanceId: number,
|
|
289
|
+
completion: Promise<unknown>,
|
|
290
|
+
): void {
|
|
291
|
+
const entry = sessionAgentsByScope.get(scopeKey);
|
|
292
|
+
if (!isActivePoolEntry(entry)) return;
|
|
293
|
+
if (entry.poolKey !== poolKey || entry.instanceId !== instanceId) return;
|
|
294
|
+
|
|
295
|
+
const completionToTrack = normalizeRunCompletion(completion);
|
|
296
|
+
const completionSettled = (entry.status === "busy"
|
|
297
|
+
? Promise.all([entry.completionSettled, completionToTrack]).then(() => undefined)
|
|
298
|
+
: completionToTrack
|
|
299
|
+
);
|
|
300
|
+
if (entry.status === "busy") {
|
|
301
|
+
entry.releaseBusyWait();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
sessionAgentsByScope.set(scopeKey, buildBusyPoolEntry(entry, completionSettled));
|
|
305
|
+
}
|
|
306
|
+
|
|
169
307
|
function leaseFromEntry(
|
|
170
|
-
entry:
|
|
308
|
+
entry: SessionCursorAgentReadyEntry,
|
|
171
309
|
scopeKey: string,
|
|
172
310
|
params: SessionCursorAgentCreateParams,
|
|
173
311
|
created: boolean,
|
|
174
312
|
): SessionCursorAgentLease {
|
|
175
313
|
bindBridgeToolRequest(entry, params.onBridgeToolRequest);
|
|
314
|
+
entry.bridgeRun?.setDebugRecorder(params.debugRecorder);
|
|
176
315
|
return {
|
|
177
316
|
scopeKey,
|
|
178
|
-
|
|
317
|
+
poolKey: entry.poolKey,
|
|
318
|
+
instanceId: entry.instanceId,
|
|
319
|
+
agent: entry.agent,
|
|
179
320
|
bridgeRun: entry.bridgeRun,
|
|
180
321
|
sendState: entry.sendState,
|
|
181
322
|
created,
|
|
323
|
+
commitSend: (context, bootstrapped) => {
|
|
324
|
+
commitSessionAgentSendForLease(scopeKey, entry.poolKey, entry.instanceId, context, bootstrapped);
|
|
325
|
+
},
|
|
326
|
+
trackRunCompletion: (completion) => {
|
|
327
|
+
trackSessionAgentRunCompletionForLease(scopeKey, entry.poolKey, entry.instanceId, completion);
|
|
328
|
+
},
|
|
182
329
|
};
|
|
183
330
|
}
|
|
184
331
|
|
|
185
|
-
|
|
332
|
+
function getCurrentReadyPoolEntry(scopeKey: string, poolKey: string): SessionCursorAgentReadyEntry | undefined {
|
|
333
|
+
const current = sessionAgentsByScope.get(scopeKey);
|
|
334
|
+
if (current?.status !== "ready") return undefined;
|
|
335
|
+
if (current.poolKey !== poolKey) return undefined;
|
|
336
|
+
return current;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function tryLeaseReadyEntry(
|
|
340
|
+
entry: SessionCursorAgentActiveEntry,
|
|
186
341
|
scopeKey: string,
|
|
342
|
+
params: SessionCursorAgentCreateParams,
|
|
187
343
|
poolKey: string,
|
|
344
|
+
created: boolean,
|
|
345
|
+
): Promise<SessionCursorAgentLease | undefined> {
|
|
346
|
+
if (entry.status === "busy") {
|
|
347
|
+
await entry.pendingCompletion;
|
|
348
|
+
}
|
|
349
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
350
|
+
if (invalidatedScopeKeys.has(scopeKey)) {
|
|
351
|
+
await disposePoolEntryForScope(scopeKey);
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
const readyEntry = getCurrentReadyPoolEntry(scopeKey, poolKey);
|
|
355
|
+
if (!readyEntry) return undefined;
|
|
356
|
+
return leaseFromEntry(readyEntry, scopeKey, params, created);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function createSessionAgentEntry(
|
|
360
|
+
scopeKey: string,
|
|
361
|
+
instanceId: number,
|
|
362
|
+
sendState: SessionCursorAgentSendState,
|
|
188
363
|
params: SessionCursorAgentCreateParams,
|
|
189
|
-
): Promise<
|
|
364
|
+
): Promise<SessionCursorAgentReadyEntry> {
|
|
190
365
|
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
191
366
|
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
192
367
|
if (registeredBridge) {
|
|
193
368
|
bridgeRun = await registeredBridge.createRun({
|
|
194
369
|
onToolRequest: params.onBridgeToolRequest,
|
|
370
|
+
debugRecorder: params.debugRecorder,
|
|
195
371
|
});
|
|
196
372
|
if (!bridgeRun.enabled || !bridgeRun.mcpServers) {
|
|
197
373
|
await bridgeRun.dispose();
|
|
@@ -222,22 +398,23 @@ async function createSessionAgentEntry(
|
|
|
222
398
|
}
|
|
223
399
|
|
|
224
400
|
return {
|
|
401
|
+
status: "ready",
|
|
225
402
|
poolKey: resolvedPoolKey,
|
|
403
|
+
instanceId,
|
|
226
404
|
scopeKey,
|
|
227
405
|
agent,
|
|
228
406
|
bridgeRun,
|
|
229
|
-
sendState
|
|
407
|
+
sendState,
|
|
230
408
|
};
|
|
231
409
|
}
|
|
232
410
|
|
|
233
|
-
export {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
411
|
+
export {
|
|
412
|
+
buildCursorSessionSendPrompt,
|
|
413
|
+
MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP,
|
|
414
|
+
planCursorSessionSend,
|
|
415
|
+
type CursorSessionSendPlan,
|
|
416
|
+
} from "./cursor-session-send-policy.js";
|
|
417
|
+
export { shouldBootstrapCursorContext, shouldBootstrapCursorSend } from "./context.js";
|
|
241
418
|
|
|
242
419
|
export function invalidateSessionAgent(scopeKey: string = getCursorSessionScopeKey()): void {
|
|
243
420
|
invalidatedScopeKeys.add(scopeKey);
|
|
@@ -245,77 +422,96 @@ export function invalidateSessionAgent(scopeKey: string = getCursorSessionScopeK
|
|
|
245
422
|
|
|
246
423
|
export async function acquireSessionCursorAgent(params: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
|
|
247
424
|
const scopeKey = getCursorSessionScopeKey();
|
|
248
|
-
assertScopeAcceptsAcquire(scopeKey);
|
|
249
|
-
if (invalidatedScopeKeys.has(scopeKey)) {
|
|
250
|
-
await disposePoolEntryForScope(scopeKey);
|
|
251
|
-
}
|
|
252
425
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
426
|
+
while (true) {
|
|
427
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
428
|
+
if (invalidatedScopeKeys.has(scopeKey)) {
|
|
429
|
+
await disposePoolEntryForScope(scopeKey);
|
|
430
|
+
}
|
|
258
431
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
432
|
+
const poolKey = buildSessionAgentPoolKey(scopeKey, params);
|
|
433
|
+
const state = getSessionCursorAgentPoolState(scopeKey);
|
|
262
434
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
435
|
+
if ((state.status === "ready" || state.status === "busy") && state.poolKey !== poolKey) {
|
|
436
|
+
await disposePoolEntryForScope(scopeKey);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (state.status === "ready") {
|
|
441
|
+
return leaseFromEntry(state, scopeKey, params, false);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (state.status === "busy") {
|
|
445
|
+
const busyGeneration = state.busyGeneration;
|
|
446
|
+
await state.pendingCompletion;
|
|
447
|
+
if (busyGeneration !== getScopeCreationGeneration(scopeKey)) continue;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (state.status === "creating") {
|
|
452
|
+
if (state.poolKey !== poolKey) {
|
|
453
|
+
await disposePoolEntryForScope(scopeKey);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
await state.creating;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error instanceof SessionCursorAgentCreationSupersededError) {
|
|
460
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
461
|
+
rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
272
464
|
throw error;
|
|
273
465
|
}
|
|
466
|
+
continue;
|
|
274
467
|
}
|
|
275
|
-
entry = sessionAgentsByScope.get(scopeKey);
|
|
276
|
-
if (entry && entry.poolKey === poolKey && entry.agent && !entry.creating) {
|
|
277
|
-
return leaseFromEntry(entry, scopeKey, params, false);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
468
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
sendState
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
469
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
470
|
+
const creationGeneration = getScopeCreationGeneration(scopeKey);
|
|
471
|
+
const instanceId = allocateSessionAgentInstanceId();
|
|
472
|
+
const sendState = createInitialSendState();
|
|
473
|
+
let placeholder: SessionCursorAgentCreatingEntry;
|
|
474
|
+
const creating = createSessionAgentEntry(scopeKey, instanceId, sendState, params).then(async (createdEntry) => {
|
|
475
|
+
const stillCurrent =
|
|
476
|
+
sessionAgentsByScope.get(scopeKey) === placeholder &&
|
|
477
|
+
getScopeCreationGeneration(scopeKey) === placeholder.creationGeneration;
|
|
478
|
+
if (!stillCurrent) {
|
|
479
|
+
await disposePoolEntry(createdEntry);
|
|
480
|
+
if (sessionAgentsByScope.get(scopeKey) === placeholder) {
|
|
481
|
+
sessionAgentsByScope.delete(scopeKey);
|
|
482
|
+
}
|
|
483
|
+
throw new SessionCursorAgentCreationSupersededError();
|
|
484
|
+
}
|
|
485
|
+
sessionAgentsByScope.set(scopeKey, createdEntry);
|
|
486
|
+
return createdEntry;
|
|
487
|
+
});
|
|
488
|
+
placeholder = {
|
|
489
|
+
status: "creating",
|
|
490
|
+
poolKey,
|
|
491
|
+
instanceId,
|
|
492
|
+
scopeKey,
|
|
493
|
+
sendState,
|
|
494
|
+
creationGeneration,
|
|
495
|
+
creating,
|
|
496
|
+
};
|
|
497
|
+
sessionAgentsByScope.set(scopeKey, placeholder);
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const createdEntry = await creating;
|
|
501
|
+
const lease = await tryLeaseReadyEntry(createdEntry, scopeKey, params, poolKey, true);
|
|
502
|
+
if (lease) return lease;
|
|
503
|
+
continue;
|
|
504
|
+
} catch (error) {
|
|
295
505
|
if (sessionAgentsByScope.get(scopeKey) === placeholder) {
|
|
296
506
|
sessionAgentsByScope.delete(scopeKey);
|
|
297
507
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
sessionAgentsByScope.set(scopeKey, placeholder);
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const createdEntry = await creating;
|
|
308
|
-
return leaseFromEntry(createdEntry, scopeKey, params, true);
|
|
309
|
-
} catch (error) {
|
|
310
|
-
if (sessionAgentsByScope.get(scopeKey) === placeholder) {
|
|
311
|
-
sessionAgentsByScope.delete(scopeKey);
|
|
312
|
-
}
|
|
313
|
-
if (error instanceof SessionCursorAgentCreationSupersededError) {
|
|
314
|
-
assertScopeAcceptsAcquire(scopeKey);
|
|
315
|
-
rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
|
|
316
|
-
return acquireSessionCursorAgent(params);
|
|
508
|
+
if (error instanceof SessionCursorAgentCreationSupersededError) {
|
|
509
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
510
|
+
rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
317
514
|
}
|
|
318
|
-
throw error;
|
|
319
515
|
}
|
|
320
516
|
}
|
|
321
517
|
|
|
@@ -361,6 +557,7 @@ export function registerCursorSessionAgent(_pi: CursorSessionAgentExtensionApi):
|
|
|
361
557
|
|
|
362
558
|
export const __testUtils = {
|
|
363
559
|
sessionAgentsByScope,
|
|
560
|
+
getSessionCursorAgentPoolState,
|
|
364
561
|
invalidateSessionAgent,
|
|
365
562
|
disposeSessionCursorAgent,
|
|
366
563
|
resetSessionCursorAgent,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
buildCursorIncrementalPrompt,
|
|
4
|
+
buildCursorPrompt,
|
|
5
|
+
shouldBootstrapCursorContext,
|
|
6
|
+
type CursorPrompt,
|
|
7
|
+
type CursorPromptOptions,
|
|
8
|
+
} from "./context.js";
|
|
9
|
+
import type { SessionCursorAgentSendState } from "./cursor-session-agent.js";
|
|
10
|
+
|
|
11
|
+
// Long-lived SDK session agents can drift tool-call behavior; recreate the agent after this many successful incremental sends.
|
|
12
|
+
export const MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP = 20;
|
|
13
|
+
|
|
14
|
+
export type CursorSessionSendMode = "bootstrap" | "incremental";
|
|
15
|
+
|
|
16
|
+
export type CursorSessionSendReason = "initial" | "context_divergence" | "incremental_threshold" | "incremental";
|
|
17
|
+
|
|
18
|
+
export interface CursorSessionSendPlan {
|
|
19
|
+
mode: CursorSessionSendMode;
|
|
20
|
+
resetAgent: boolean;
|
|
21
|
+
reason: CursorSessionSendReason;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function planCursorSessionSend(sendState: SessionCursorAgentSendState, context: Context): CursorSessionSendPlan {
|
|
25
|
+
if (!sendState.bootstrapped) {
|
|
26
|
+
return { mode: "bootstrap", resetAgent: false, reason: "initial" };
|
|
27
|
+
}
|
|
28
|
+
if (sendState.incrementalSendCount >= MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP) {
|
|
29
|
+
return { mode: "bootstrap", resetAgent: true, reason: "incremental_threshold" };
|
|
30
|
+
}
|
|
31
|
+
if (shouldBootstrapCursorContext(sendState, context)) {
|
|
32
|
+
return { mode: "bootstrap", resetAgent: true, reason: "context_divergence" };
|
|
33
|
+
}
|
|
34
|
+
return { mode: "incremental", resetAgent: false, reason: "incremental" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCursorSessionSendPrompt(
|
|
38
|
+
context: Context,
|
|
39
|
+
options: CursorPromptOptions,
|
|
40
|
+
plan: CursorSessionSendPlan,
|
|
41
|
+
): CursorPrompt {
|
|
42
|
+
return plan.mode === "bootstrap" ? buildCursorPrompt(context, options) : buildCursorIncrementalPrompt(context, options);
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SettingSource } from "@cursor/sdk";
|
|
2
|
+
|
|
3
|
+
export const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
4
|
+
|
|
5
|
+
export function resolveCursorSettingSources(raw?: string): SettingSource[] | undefined {
|
|
6
|
+
const trimmed = raw?.trim();
|
|
7
|
+
if (!trimmed) return ["all"];
|
|
8
|
+
const normalized = trimmed.toLowerCase();
|
|
9
|
+
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
10
|
+
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
11
|
+
return trimmed
|
|
12
|
+
.split(",")
|
|
13
|
+
.map((entry) => entry.trim())
|
|
14
|
+
.filter((entry): entry is SettingSource => Boolean(entry));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getEffectiveCursorSettingSources(raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV]): SettingSource[] | undefined {
|
|
18
|
+
return resolveCursorSettingSources(raw);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cursorSettingSourcesLoadUserAgentsRules(settingSources: SettingSource[] | undefined): boolean {
|
|
22
|
+
if (!settingSources?.length) return false;
|
|
23
|
+
return settingSources.includes("all") || settingSources.includes("user");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cursorSettingSourcesLoadProjectAgentsRules(settingSources: SettingSource[] | undefined): boolean {
|
|
27
|
+
if (!settingSources?.length) return false;
|
|
28
|
+
return settingSources.includes("all") || settingSources.includes("project");
|
|
29
|
+
}
|
package/src/cursor-state.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { isCursorModel } from "./cursor-model.js";
|
|
5
6
|
import { getCursorModelMetadata } from "./model-discovery.js";
|
|
6
7
|
|
|
7
|
-
const CURSOR_PROVIDER = "cursor";
|
|
8
8
|
const FAST_ENTRY_TYPE = "cursor-fast-state";
|
|
9
9
|
const GLOBAL_CONFIG_FILE = "cursor-sdk.json";
|
|
10
10
|
|
|
@@ -94,10 +94,6 @@ function getEffectiveFast(baseModelId: string, modelId: string): boolean | undef
|
|
|
94
94
|
return sessionFastPreferences.get(baseModelId) ?? globalFastPreferences.get(baseModelId) ?? metadata.defaultFast;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function isCursorModel(model: CursorFastControlsModel): boolean {
|
|
98
|
-
return model?.provider === CURSOR_PROVIDER || model?.api === "cursor-sdk";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
97
|
function updateCursorStatus(ctx: { model: CursorFastControlsModel; ui: Pick<ExtensionContext["ui"], "setStatus"> }, model = ctx.model): void {
|
|
102
98
|
if (!model || !isCursorModel(model)) {
|
|
103
99
|
ctx.ui.setStatus("cursor", undefined);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
2
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
3
|
+
import { extractWebSearchQuery } from "./cursor-web-tool-activity.js";
|
|
4
|
+
import { firstNonEmptyLine, getArray, getString, truncateArg } from "./cursor-transcript-utils.js";
|
|
5
|
+
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
6
|
+
|
|
7
|
+
/** Defer pending lifecycle lines so fast start+complete pairs coalesce into the completed replay card only. */
|
|
8
|
+
export const CURSOR_TOOL_LIFECYCLE_DEFER_MS = 75;
|
|
9
|
+
|
|
10
|
+
export function isCursorToolLifecycleEligible(toolCall: unknown): boolean {
|
|
11
|
+
return classifyCursorToolVisibility(toolCall).lifecycleEligible;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getCursorToolLifecycleTitle(toolCall: unknown): string {
|
|
15
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
16
|
+
return visibility.lifecycleTitle ?? `Cursor ${visibility.normalizedName}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Prefixes that commonly introduce path/URI values in free-text pending lifecycle details. */
|
|
20
|
+
const LIFECYCLE_DETAIL_PATH_PREFIX = String.raw`(?:^|[\s'"({=,:;\[\]{}])`;
|
|
21
|
+
|
|
22
|
+
function containsCursorLifecycleUnsafeDetail(text: string): boolean {
|
|
23
|
+
if (/\b[a-z][a-z0-9+.-]*:\/\//i.test(text)) return true;
|
|
24
|
+
if (/\bwww\.\S+/i.test(text)) return true;
|
|
25
|
+
if (new RegExp(`${LIFECYCLE_DETAIL_PATH_PREFIX}~\\/\\S*`).test(text)) return true;
|
|
26
|
+
if (new RegExp(`${LIFECYCLE_DETAIL_PATH_PREFIX}\\/\\S+`).test(text)) return true;
|
|
27
|
+
if (new RegExp(`${LIFECYCLE_DETAIL_PATH_PREFIX}[A-Za-z]:[\\\\/]`).test(text)) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function scrubLifecycleDetail(value: string | undefined, apiKey?: string): string | undefined {
|
|
32
|
+
if (!value?.trim()) return undefined;
|
|
33
|
+
const scrubbed = truncateCursorDisplayLine(scrubSensitiveText(value, apiKey));
|
|
34
|
+
if (containsCursorLifecycleUnsafeDetail(scrubbed)) return undefined;
|
|
35
|
+
return scrubbed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string): string | undefined {
|
|
39
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
40
|
+
const args = visibility.args;
|
|
41
|
+
|
|
42
|
+
switch (visibility.normalizedKey) {
|
|
43
|
+
case "task": {
|
|
44
|
+
return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
|
|
45
|
+
}
|
|
46
|
+
case "shell": {
|
|
47
|
+
return "shell";
|
|
48
|
+
}
|
|
49
|
+
case "mcp": {
|
|
50
|
+
return scrubLifecycleDetail(getString(args, "toolName"), apiKey) ?? "mcp";
|
|
51
|
+
}
|
|
52
|
+
case "generateimage": {
|
|
53
|
+
return scrubLifecycleDetail(getString(args, "prompt") ?? getString(args, "description"), apiKey) ?? "image generation";
|
|
54
|
+
}
|
|
55
|
+
case "recordscreen": {
|
|
56
|
+
return scrubLifecycleDetail(getString(args, "mode"), apiKey) ?? "screen recording";
|
|
57
|
+
}
|
|
58
|
+
case "semsearch": {
|
|
59
|
+
return scrubLifecycleDetail(getString(args, "query"), apiKey) ?? "semantic search";
|
|
60
|
+
}
|
|
61
|
+
case "websearch": {
|
|
62
|
+
return scrubLifecycleDetail(extractWebSearchQuery(args), apiKey) ?? "web search";
|
|
63
|
+
}
|
|
64
|
+
case "webfetch": {
|
|
65
|
+
return "web fetch";
|
|
66
|
+
}
|
|
67
|
+
case "createplan": {
|
|
68
|
+
const plan = getString(args, "plan");
|
|
69
|
+
return scrubLifecycleDetail(plan ? firstNonEmptyLine(plan) ?? plan : undefined, apiKey) ?? "plan";
|
|
70
|
+
}
|
|
71
|
+
case "updatetodos": {
|
|
72
|
+
const todos = getArray(args, "todos") ?? getArray(args, "items");
|
|
73
|
+
if (todos && todos.length > 0) return truncateArg(`${todos.length} item${todos.length === 1 ? "" : "s"}`);
|
|
74
|
+
return "todos";
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatCursorToolLifecycleProgressText(toolCall: unknown, apiKey?: string): string | undefined {
|
|
82
|
+
const label = buildCursorToolLifecycleLabel(toolCall, apiKey);
|
|
83
|
+
if (!label) return undefined;
|
|
84
|
+
return `${getCursorToolLifecycleTitle(toolCall)}: ${label}\n`;
|
|
85
|
+
}
|