pi-cursor-sdk 0.1.18 → 0.1.19

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 (46) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +37 -0
  3. package/docs/cursor-live-smoke-checklist.md +3 -0
  4. package/docs/cursor-model-ux-spec.md +4 -3
  5. package/docs/cursor-native-tool-replay.md +96 -2
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +8 -2
  8. package/scripts/debug-provider-events.mjs +403 -0
  9. package/scripts/debug-sdk-events.mjs +413 -0
  10. package/scripts/lib/cursor-probe-utils.mjs +52 -0
  11. package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
  12. package/scripts/validate-smoke-jsonl.mjs +27 -3
  13. package/src/context.ts +45 -32
  14. package/src/cursor-agent-message-web-tools.ts +172 -0
  15. package/src/cursor-agents-context.ts +176 -0
  16. package/src/cursor-incomplete-tool-visibility.ts +118 -0
  17. package/src/cursor-live-run-coordinator.ts +18 -7
  18. package/src/cursor-model.ts +12 -0
  19. package/src/cursor-native-tool-display-registration.ts +1 -4
  20. package/src/cursor-native-tool-display-replay.ts +63 -5
  21. package/src/cursor-native-tool-display-tools.ts +20 -0
  22. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  23. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  24. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  25. package/src/cursor-provider-errors.ts +96 -0
  26. package/src/cursor-provider-live-run-drain.ts +181 -62
  27. package/src/cursor-provider-turn-coordinator.ts +198 -32
  28. package/src/cursor-provider.ts +270 -83
  29. package/src/cursor-question-tool.ts +1 -4
  30. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  31. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  32. package/src/cursor-sdk-event-debug-session.ts +163 -0
  33. package/src/cursor-sdk-event-debug.ts +597 -0
  34. package/src/cursor-sensitive-text.ts +27 -7
  35. package/src/cursor-session-agent.ts +25 -3
  36. package/src/cursor-session-send-policy.ts +43 -0
  37. package/src/cursor-setting-sources.ts +29 -0
  38. package/src/cursor-state.ts +1 -5
  39. package/src/cursor-tool-lifecycle.ts +111 -0
  40. package/src/cursor-tool-names.ts +12 -0
  41. package/src/cursor-tool-transcript.ts +4 -2
  42. package/src/cursor-transcript-tool-formatters.ts +228 -5
  43. package/src/cursor-transcript-tool-specs.ts +113 -14
  44. package/src/cursor-transcript-utils.ts +12 -0
  45. package/src/cursor-web-tool-activity.ts +84 -0
  46. package/src/index.ts +4 -1
@@ -9,7 +9,22 @@ import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js
9
9
  import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
10
10
  import { scrubPiToolDisplay, scrubSensitiveText } from "./cursor-sensitive-text.js";
11
11
  import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
12
- import { getString, getToolArgs, getToolName } from "./cursor-transcript-utils.js";
12
+ import {
13
+ recordDiscardedIncompleteStartedToolCall,
14
+ type CursorSdkEventDebugRecorder,
15
+ DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
16
+ } from "./cursor-sdk-event-debug.js";
17
+ import {
18
+ buildIncompleteCursorToolDisplay,
19
+ formatIncompleteCursorToolTrace,
20
+ type IncompleteCursorToolDiscardReason,
21
+ } from "./cursor-incomplete-tool-visibility.js";
22
+ import { getToolName, normalizeToolName } from "./cursor-transcript-utils.js";
23
+ import {
24
+ CURSOR_TOOL_LIFECYCLE_DEFER_MS,
25
+ formatCursorToolLifecycleProgressText,
26
+ isCursorToolLifecycleEligible,
27
+ } from "./cursor-tool-lifecycle.js";
13
28
 
14
29
  function formatCursorToolName(toolCall: unknown): string {
15
30
  return truncateCursorDisplayLine(getToolName(toolCall), 80) || "unknown";
@@ -30,17 +45,6 @@ function isCursorShellToolCall(toolCall: unknown): boolean {
30
45
  return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
31
46
  }
32
47
 
33
- function isCursorTaskToolCall(toolCall: unknown): boolean {
34
- return getToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase() === "task";
35
- }
36
-
37
- function extractCursorTaskProgressLabel(toolCall: unknown, apiKey?: string): string | undefined {
38
- if (!isCursorTaskToolCall(toolCall)) return undefined;
39
- const description = getString(getToolArgs(toolCall), "description");
40
- if (!description?.trim()) return undefined;
41
- return truncateCursorDisplayLine(scrubSensitiveText(description, apiKey));
42
- }
43
-
44
48
  function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
45
49
  if (update.type !== "shell-output-delta") return undefined;
46
50
  const event = getField(update, "event");
@@ -83,13 +87,15 @@ function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: Cur
83
87
  };
84
88
  }
85
89
 
90
+ type CursorToolDisplaySource = "started" | "fallback" | "transcript";
91
+
86
92
  type ToolCompletionResolution =
87
93
  | { action: "ignore-bridge"; identity?: string }
88
94
  | {
89
95
  action: "handle";
90
96
  toolCall: unknown;
91
97
  identity?: string;
92
- source?: "started" | "fallback";
98
+ source?: CursorToolDisplaySource;
93
99
  matchedStartedCallId?: string;
94
100
  };
95
101
 
@@ -103,6 +109,7 @@ export interface CursorSdkTurnCoordinatorOptions {
103
109
  activeToolNames?: ReadonlySet<string>;
104
110
  nativeReplayId: string;
105
111
  textDeltas: string[];
112
+ debugRecorder?: CursorSdkEventDebugRecorder;
106
113
  }
107
114
 
108
115
  export class CursorSdkTurnCoordinator {
@@ -117,6 +124,7 @@ export class CursorSdkTurnCoordinator {
117
124
  readonly textDeltas: string[];
118
125
 
119
126
  private readonly contentEmitter: CursorPartialContentEmitter;
127
+ private readonly debugRecorder?: CursorSdkEventDebugRecorder;
120
128
  private nativeToolDisplayCounter = 0;
121
129
  private nativeToolReplayStarted = false;
122
130
  private cursorPlanTextCandidate: string | undefined;
@@ -128,7 +136,8 @@ export class CursorSdkTurnCoordinator {
128
136
  private readonly completedToolIdentities = new Set<string>();
129
137
  private readonly completedStartedToolFingerprints = new Set<string>();
130
138
  private readonly completedFallbackToolFingerprints = new Set<string>();
131
- private readonly emittedTaskProgressCallIds = new Set<string>();
139
+ private readonly emittedLifecycleCallIds = new Set<string>();
140
+ private readonly lifecycleTimers = new Map<string, ReturnType<typeof setTimeout>>();
132
141
 
133
142
  constructor(options: CursorSdkTurnCoordinatorOptions) {
134
143
  this.stream = options.stream;
@@ -140,6 +149,7 @@ export class CursorSdkTurnCoordinator {
140
149
  this.activeToolNames = options.activeToolNames;
141
150
  this.nativeReplayId = options.nativeReplayId;
142
151
  this.textDeltas = options.textDeltas;
152
+ this.debugRecorder = options.debugRecorder;
143
153
  this.contentEmitter = new CursorPartialContentEmitter(options.stream, options.partial, undefined, false);
144
154
  }
145
155
 
@@ -151,13 +161,26 @@ export class CursorSdkTurnCoordinator {
151
161
  return this.nativeToolReplayStarted;
152
162
  }
153
163
 
154
- discardIncompleteStartedToolCalls(): void {
164
+ discardIncompleteStartedToolCalls(
165
+ reason: IncompleteCursorToolDiscardReason = DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
166
+ ): void {
167
+ for (const [callId, toolCall] of this.startedToolCalls) {
168
+ if (typeof callId !== "string") continue;
169
+ recordDiscardedIncompleteStartedToolCall(this.debugRecorder, process.env, {
170
+ toolName: normalizeToolName(getToolName(toolCall)),
171
+ callId,
172
+ reason,
173
+ });
174
+ this.emitIncompleteStartedToolCall(toolCall, reason);
175
+ }
155
176
  this.startedToolCalls.clear();
156
177
  this.bridgeStartedToolCallIds.clear();
157
178
  this.activeShellCallIds.clear();
158
179
  this.ambiguousShellOutputCallIds.clear();
159
180
  this.shellOutputDeltasByCallId.clear();
160
- this.emittedTaskProgressCallIds.clear();
181
+ this.emittedLifecycleCallIds.clear();
182
+ for (const timer of this.lifecycleTimers.values()) clearTimeout(timer);
183
+ this.lifecycleTimers.clear();
161
184
  }
162
185
 
163
186
  closeTraceBlock(): void {
@@ -195,14 +218,14 @@ export class CursorSdkTurnCoordinator {
195
218
  return;
196
219
  }
197
220
  if (update.type === "partial-tool-call") {
198
- this.maybeEmitCursorTaskProgress(update.callId, update.toolCall);
221
+ this.maybeScheduleCursorToolLifecycle(update.callId, update.toolCall);
199
222
  return;
200
223
  }
201
224
  if (update.type === "tool-call-started") {
202
225
  if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
203
226
  if (typeof update.callId === "string") this.bridgeStartedToolCallIds.add(update.callId);
204
227
  } else {
205
- this.maybeEmitCursorTaskProgress(update.callId, update.toolCall);
228
+ this.maybeScheduleCursorToolLifecycle(update.callId, update.toolCall);
206
229
  this.startedToolCalls.set(update.callId, update.toolCall);
207
230
  if (isCursorShellToolCall(update.toolCall)) this.activeShellCallIds.add(update.callId);
208
231
  }
@@ -215,7 +238,10 @@ export class CursorSdkTurnCoordinator {
215
238
  toolCall: update.toolCall,
216
239
  startedToolCall: this.startedToolCalls.get(update.callId),
217
240
  });
218
- if (resolution.action === "ignore-bridge") return;
241
+ if (resolution.action === "ignore-bridge") {
242
+ this.recordIgnoreBridgeDecision(resolution.identity, getToolName(update.toolCall), "delta");
243
+ return;
244
+ }
219
245
  this.handleCompletedToolCall(resolution.toolCall, {
220
246
  identity: resolution.identity,
221
247
  source: resolution.source,
@@ -251,7 +277,10 @@ export class CursorSdkTurnCoordinator {
251
277
  callId: stepId,
252
278
  toolCall,
253
279
  });
254
- if (resolution.action === "ignore-bridge") return;
280
+ if (resolution.action === "ignore-bridge") {
281
+ this.recordIgnoreBridgeDecision(resolution.identity, getToolName(toolCall), "step");
282
+ return;
283
+ }
255
284
  this.handleCompletedToolCall(resolution.toolCall, {
256
285
  identity: resolution.identity,
257
286
  source: resolution.source,
@@ -319,20 +348,52 @@ export class CursorSdkTurnCoordinator {
319
348
  };
320
349
  }
321
350
 
351
+ handleTranscriptCompletedToolCalls(toolCalls: readonly { identity: string; toolCall: unknown }[]): void {
352
+ for (const { identity, toolCall } of toolCalls) {
353
+ this.handleCompletedToolCall(toolCall, { identity, source: "transcript" });
354
+ }
355
+ }
356
+
322
357
  private handleCompletedToolCall(
323
358
  toolCall: unknown,
324
- options: { identity?: string; source?: "started" | "fallback" } = {},
359
+ options: { identity?: string; source?: CursorToolDisplaySource } = {},
325
360
  ): void {
326
361
  const planText = getCursorCreatePlanText(toolCall);
327
362
  if (planText) this.cursorPlanTextCandidate = scrubSensitiveText(planText, this.resolvedApiKey);
328
363
 
329
364
  const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd: this.cwd }), this.resolvedApiKey);
330
365
  const display = buildCursorPiToolDisplay(toolCall, { cwd: this.cwd });
366
+ const toolName = display.toolName;
331
367
  const fingerprint = this.getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
332
- if (options.identity && this.completedToolIdentities.has(options.identity)) return;
368
+ if (options.identity && this.completedToolIdentities.has(options.identity)) {
369
+ this.recordDisplayDecision({
370
+ action: "skip-duplicate",
371
+ toolName,
372
+ identity: options.identity,
373
+ source: options.source,
374
+ reason: "identity-already-completed",
375
+ });
376
+ return;
377
+ }
333
378
  if (options.source === "started") {
334
- if (this.completedFallbackToolFingerprints.has(fingerprint)) return;
379
+ if (this.completedFallbackToolFingerprints.has(fingerprint)) {
380
+ this.recordDisplayDecision({
381
+ action: "skip-duplicate",
382
+ toolName,
383
+ identity: options.identity,
384
+ source: options.source,
385
+ reason: "fallback-fingerprint-already-completed",
386
+ });
387
+ return;
388
+ }
335
389
  } else if (this.completedStartedToolFingerprints.has(fingerprint) || this.completedFallbackToolFingerprints.has(fingerprint)) {
390
+ this.recordDisplayDecision({
391
+ action: "skip-duplicate",
392
+ toolName,
393
+ identity: options.identity,
394
+ source: options.source,
395
+ reason: "fingerprint-already-completed",
396
+ });
336
397
  return;
337
398
  }
338
399
  if (options.identity) this.completedToolIdentities.add(options.identity);
@@ -353,6 +414,15 @@ export class CursorSdkTurnCoordinator {
353
414
  this.nativeToolReplayStarted = true;
354
415
  const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
355
416
  const scrubbedDisplay = scrubPiToolDisplay(display, this.resolvedApiKey);
417
+ this.recordDisplayDecision({
418
+ action: "queue_replay",
419
+ disposition,
420
+ toolName,
421
+ identity: options.identity,
422
+ source: options.source,
423
+ transcript,
424
+ replayToolId: id,
425
+ });
356
426
  cursorLiveRuns.queueEvent(this.liveRun, {
357
427
  type: "tool",
358
428
  tool: { ...scrubbedDisplay, id },
@@ -362,11 +432,84 @@ export class CursorSdkTurnCoordinator {
362
432
 
363
433
  const traceText =
364
434
  disposition === "inactive_trace"
365
- ? formatInactiveCursorReplayTrace(display)
435
+ ? formatInactiveCursorReplayTrace(scrubPiToolDisplay(display, this.resolvedApiKey))
366
436
  : transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`;
437
+ this.recordDisplayDecision({
438
+ action: "emit_trace",
439
+ disposition,
440
+ toolName,
441
+ identity: options.identity,
442
+ source: options.source,
443
+ transcript,
444
+ traceText,
445
+ });
446
+ this.emitCursorToolTrace(traceText);
447
+ }
448
+
449
+ private emitIncompleteStartedToolCall(toolCall: unknown, reason: IncompleteCursorToolDiscardReason): void {
450
+ const display = scrubPiToolDisplay(
451
+ buildIncompleteCursorToolDisplay(toolCall, reason, { apiKey: this.resolvedApiKey }),
452
+ this.resolvedApiKey,
453
+ );
454
+ const toolName = display.toolName;
455
+ const disposition = resolveNativeReplayDisposition({
456
+ toolName,
457
+ useNativeToolReplay: this.useNativeToolReplay,
458
+ activeToolNames: this.activeToolNames,
459
+ hasLiveRun: this.liveRun !== undefined,
460
+ });
461
+
462
+ // Aborted live runs emit trace visibility only; do not synthesize a toolUse replay turn.
463
+ if (disposition === "queue_replay" && this.liveRun && reason !== "abort") {
464
+ this.nativeToolReplayStarted = true;
465
+ const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
466
+ this.recordDisplayDecision({
467
+ action: "queue_replay",
468
+ disposition,
469
+ toolName,
470
+ source: "started",
471
+ reason: "incomplete-started-tool-call",
472
+ replayToolId: id,
473
+ });
474
+ cursorLiveRuns.queueEvent(this.liveRun, {
475
+ type: "tool",
476
+ tool: { ...display, id },
477
+ });
478
+ return;
479
+ }
480
+
481
+ const traceText =
482
+ disposition === "inactive_trace"
483
+ ? formatInactiveCursorReplayTrace(display)
484
+ : formatIncompleteCursorToolTrace(display);
485
+ this.recordDisplayDecision({
486
+ action: "emit_trace",
487
+ disposition,
488
+ toolName,
489
+ source: "started",
490
+ reason: "incomplete-started-tool-call",
491
+ traceText,
492
+ });
367
493
  this.emitCursorToolTrace(traceText);
368
494
  }
369
495
 
496
+ private recordIgnoreBridgeDecision(
497
+ identity: string | undefined,
498
+ toolName: string,
499
+ source: "delta" | "step",
500
+ ): void {
501
+ this.debugRecorder?.recordDisplayDecision({
502
+ action: "ignore-bridge",
503
+ toolName,
504
+ identity,
505
+ source,
506
+ });
507
+ }
508
+
509
+ private recordDisplayDecision(decision: Parameters<CursorSdkEventDebugRecorder["recordDisplayDecision"]>[0]): void {
510
+ this.debugRecorder?.recordDisplayDecision(decision);
511
+ }
512
+
370
513
  private emitCursorToolTrace(text: string): void {
371
514
  const traceText = text.endsWith("\n") ? text : `${text}\n`;
372
515
  if (this.liveRun) {
@@ -377,17 +520,39 @@ export class CursorSdkTurnCoordinator {
377
520
  this.contentEmitter.appendThinkingBlock(traceText);
378
521
  }
379
522
 
380
- private maybeEmitCursorTaskProgress(callId: unknown, toolCall: unknown): void {
381
- if (typeof callId !== "string" || this.emittedTaskProgressCallIds.has(callId)) return;
523
+ private maybeScheduleCursorToolLifecycle(callId: unknown, toolCall: unknown): void {
524
+ if (typeof callId !== "string" || this.emittedLifecycleCallIds.has(callId)) return;
382
525
  if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) return;
383
- const label = extractCursorTaskProgressLabel(toolCall, this.resolvedApiKey);
384
- if (!label) return;
385
- this.emittedTaskProgressCallIds.add(callId);
386
- this.emitCursorTaskProgress(label);
526
+ if (!isCursorToolLifecycleEligible(toolCall)) return;
527
+
528
+ this.cancelCursorToolLifecycleTimer(callId);
529
+ const timer = setTimeout(() => {
530
+ this.lifecycleTimers.delete(callId);
531
+ if (!this.startedToolCalls.has(callId)) return;
532
+ if (this.emittedLifecycleCallIds.has(callId)) return;
533
+ this.emitCursorToolLifecycle(callId, toolCall);
534
+ }, CURSOR_TOOL_LIFECYCLE_DEFER_MS);
535
+ timer.unref?.();
536
+ this.lifecycleTimers.set(callId, timer);
387
537
  }
388
538
 
389
- private emitCursorTaskProgress(label: string): void {
390
- const progressText = `Cursor task: ${label}\n`;
539
+ private cancelCursorToolLifecycleTimer(callId: string): void {
540
+ const timer = this.lifecycleTimers.get(callId);
541
+ if (!timer) return;
542
+ clearTimeout(timer);
543
+ this.lifecycleTimers.delete(callId);
544
+ }
545
+
546
+ private emitCursorToolLifecycle(callId: string, toolCall: unknown): void {
547
+ const progressText = formatCursorToolLifecycleProgressText(toolCall, this.resolvedApiKey);
548
+ if (!progressText) return;
549
+ this.emittedLifecycleCallIds.add(callId);
550
+ this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle", {
551
+ callId,
552
+ toolName: normalizeToolName(getToolName(toolCall)),
553
+ progressText,
554
+ liveRun: this.liveRun !== undefined,
555
+ });
391
556
  if (this.liveRun) {
392
557
  cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: progressText });
393
558
  return;
@@ -408,6 +573,7 @@ export class CursorSdkTurnCoordinator {
408
573
  }
409
574
 
410
575
  private clearStartedToolCall(callId: string): void {
576
+ this.cancelCursorToolLifecycleTimer(callId);
411
577
  this.startedToolCalls.delete(callId);
412
578
  this.bridgeStartedToolCallIds.delete(callId);
413
579
  this.activeShellCallIds.delete(callId);