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
@@ -23,8 +23,10 @@ import { resetSessionCursorAgent } from "./cursor-session-agent.js";
23
23
  import { applyCursorApproximateUsage } from "./cursor-usage-accounting.js";
24
24
  import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
25
25
  import { hasUsableText } from "./cursor-record-utils.js";
26
+ import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
26
27
  import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
27
28
  import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
29
+ import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
28
30
 
29
31
  export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
30
32
  const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
@@ -103,6 +105,37 @@ export async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<voi
103
105
  await scheduler.wait(75);
104
106
  }
105
107
 
108
+ export function flushPendingCursorLiveRunTraceEventsToStream(
109
+ stream: AssistantMessageEventStream,
110
+ partial: AssistantMessage,
111
+ run: CursorLiveRun,
112
+ options?: { includeTracesBehindQueuedTools?: boolean },
113
+ ): void {
114
+ if (run.disposed) return;
115
+ const turn: CursorLiveTurnState = {
116
+ emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
117
+ emittedText: "",
118
+ };
119
+ while (true) {
120
+ const event = cursorLiveRuns.peekEvent(run);
121
+ if (!event || event.type === "tool" || event.type === "bridge-tool") break;
122
+ cursorLiveRuns.shiftEvent(run);
123
+ emitCursorLiveQueuedEvent(turn, event, run);
124
+ }
125
+ if (options?.includeTracesBehindQueuedTools && run.pendingEvents.length > 0) {
126
+ const preserved: CursorLiveQueuedEvent[] = [];
127
+ for (const event of run.pendingEvents) {
128
+ if (event.type === "tool" || event.type === "bridge-tool") {
129
+ preserved.push(event);
130
+ continue;
131
+ }
132
+ emitCursorLiveQueuedEvent(turn, event, run);
133
+ }
134
+ run.pendingEvents = preserved;
135
+ }
136
+ turn.emitter.closeAll();
137
+ }
138
+
106
139
  function emitCursorLiveQueuedEvent(
107
140
  turn: CursorLiveTurnState,
108
141
  event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
@@ -178,6 +211,7 @@ function emitCursorNativeToolUseTurn(
178
211
  run: CursorLiveRun,
179
212
  toolResultInputTokens: number,
180
213
  tools: CursorNativeToolDisplayItem[],
214
+ debugRecorder?: CursorSdkEventDebugRecorder,
181
215
  ): void {
182
216
  const shouldTerminate = run.done && !run.finalText?.trim() && !cursorLiveRuns.peekEvent(run);
183
217
  for (const tool of tools) {
@@ -194,6 +228,11 @@ function emitCursorNativeToolUseTurn(
194
228
  if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
195
229
  if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
196
230
  run.recordedToolDisplayIds.push(tool.id);
231
+ debugRecorder?.recordDrainEvent("native_tool_display_recorded", {
232
+ toolId: tool.id,
233
+ toolName: tool.toolName,
234
+ terminate: shouldTerminate,
235
+ });
197
236
  }
198
237
  }
199
238
  applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
@@ -202,10 +241,20 @@ function emitCursorNativeToolUseTurn(
202
241
  cursorLiveRuns.requestIdleDispose(run);
203
242
  }
204
243
 
205
- function emitInactiveCursorReplayTrace(turn: CursorLiveTurnState, tools: CursorNativeToolDisplayItem[]): void {
244
+ function emitInactiveCursorReplayTrace(
245
+ turn: CursorLiveTurnState,
246
+ tools: CursorNativeToolDisplayItem[],
247
+ debugRecorder?: CursorSdkEventDebugRecorder,
248
+ ): void {
206
249
  if (tools.length === 0) return;
207
250
  for (const tool of tools) {
208
- turn.emitter.appendThinkingBlock(formatInactiveCursorReplayTrace(tool));
251
+ const traceText = formatInactiveCursorReplayTrace(tool);
252
+ debugRecorder?.recordDrainEvent("inactive_replay_trace", {
253
+ toolId: tool.id,
254
+ toolName: tool.toolName,
255
+ traceText,
256
+ });
257
+ turn.emitter.appendThinkingBlock(traceText);
209
258
  }
210
259
  }
211
260
 
@@ -245,21 +294,22 @@ async function emitCursorLiveRunPendingToolUseTurn(
245
294
  context: Context,
246
295
  run: CursorLiveRun,
247
296
  toolResultInputTokens: number,
248
- options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal },
297
+ options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
249
298
  ): Promise<"tool_use" | "handled" | undefined> {
299
+ const debugRecorder = options.debugRecorder ?? run.debugRecorder;
250
300
  const eventType = cursorLiveRuns.peekEvent(run)?.type;
251
301
  if (eventType !== "tool" && eventType !== "bridge-tool") return undefined;
252
302
  await settleCursorLiveToolBatch(run);
253
303
  if (options.signal?.aborted) throw new CursorLiveRunAbortError();
254
304
  if (eventType === "tool") {
255
305
  const { active, inactive } = partitionNativeToolsByActiveContext(context, cursorLiveRuns.collectNativeToolBatch(run));
256
- if (options.mode === "emit") emitInactiveCursorReplayTrace(turn, inactive);
306
+ if (options.mode === "emit") emitInactiveCursorReplayTrace(turn, inactive, debugRecorder);
257
307
  if (active.length === 0) {
258
308
  // Inactive-only batch: trace was emitted above; do not emit toolUse.
259
309
  return "handled";
260
310
  }
261
311
  if (options.mode === "emit") turn.emitter.closeAll();
262
- emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active);
312
+ emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active, debugRecorder);
263
313
  } else {
264
314
  if (options.mode === "emit") turn.emitter.closeAll();
265
315
  const requests = cursorLiveRuns.collectBridgeToolBatch(run);
@@ -275,73 +325,123 @@ export async function drainCursorLiveRunTurn(
275
325
  context: Context,
276
326
  run: CursorLiveRun,
277
327
  toolResultInputTokens: number,
278
- options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal },
328
+ options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
279
329
  ): Promise<CursorLiveRunDrainOutcome> {
330
+ const debugRecorder = options.debugRecorder ?? run.debugRecorder;
331
+ debugRecorder?.recordDrainEvent("turn_start", {
332
+ mode: options.mode,
333
+ runId: run.id,
334
+ pendingEventCount: run.pendingEvents.length,
335
+ done: run.done,
336
+ });
337
+ let outcome: CursorLiveRunDrainOutcome | undefined;
338
+ let outcomeDetails: Record<string, unknown> = {};
280
339
  const turn: CursorLiveTurnState = {
281
340
  emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
282
341
  emittedText: "",
283
342
  };
284
343
 
285
- while (true) {
286
- if (options.mode === "chain_user_input" && cursorLiveRuns.isReady(run)) {
287
- await cursorLiveRuns.release(run);
288
- return "chain_user_input";
289
- }
344
+ try {
345
+ while (true) {
346
+ if (options.mode === "chain_user_input" && cursorLiveRuns.isReady(run)) {
347
+ await cursorLiveRuns.release(run);
348
+ outcome = "chain_user_input";
349
+ return outcome;
350
+ }
290
351
 
291
- while (cursorLiveRuns.peekEvent(run)) {
292
- const toolUse = await emitCursorLiveRunPendingToolUseTurn(
293
- turn,
294
- stream,
295
- partial,
296
- model,
297
- context,
298
- run,
299
- toolResultInputTokens,
300
- options,
301
- );
302
- if (toolUse === "tool_use") return toolUse;
303
- if (toolUse === "handled") continue;
304
- const event = cursorLiveRuns.shiftEvent(run);
305
- if (!event || event.type === "tool" || event.type === "bridge-tool") continue;
306
- if (options.mode === "emit") emitCursorLiveQueuedEvent(turn, event, run);
307
- }
352
+ while (cursorLiveRuns.peekEvent(run)) {
353
+ const toolUse = await emitCursorLiveRunPendingToolUseTurn(
354
+ turn,
355
+ stream,
356
+ partial,
357
+ model,
358
+ context,
359
+ run,
360
+ toolResultInputTokens,
361
+ options,
362
+ );
363
+ if (toolUse === "tool_use") {
364
+ outcome = "tool_use";
365
+ return outcome;
366
+ }
367
+ if (toolUse === "handled") continue;
368
+ const event = cursorLiveRuns.shiftEvent(run);
369
+ if (!event || event.type === "tool" || event.type === "bridge-tool") continue;
370
+ if (options.mode === "emit") emitCursorLiveQueuedEvent(turn, event, run);
371
+ }
308
372
 
309
- if (run.disposed) {
310
- partial.stopReason = "aborted";
311
- stream.push({ type: "error", reason: "aborted", error: partial });
312
- return "aborted";
313
- }
314
- if (run.cancelled) {
315
- partial.stopReason = "aborted";
316
- stream.push({ type: "error", reason: "aborted", error: partial });
317
- await cursorLiveRuns.release(run);
318
- return "aborted";
319
- }
320
- if (run.errorMessage) {
321
- partial.stopReason = "error";
322
- partial.errorMessage = run.errorMessage;
323
- stream.push({ type: "error", reason: "error", error: partial });
324
- await cursorLiveRuns.release(run);
325
- return "error";
326
- }
327
- if (run.done) {
328
- if (options.mode === "chain_user_input") {
373
+ if (run.disposed) {
374
+ partial.stopReason = "aborted";
375
+ partial.errorMessage = formatCursorSdkAbortMessage(
376
+ resolveCursorSdkAbortCause({ liveRunDisposed: true }),
377
+ );
378
+ stream.push({ type: "error", reason: "aborted", error: partial });
379
+ outcome = "aborted";
380
+ outcomeDetails = { reason: "disposed" };
381
+ return outcome;
382
+ }
383
+ if (run.cancelled) {
384
+ partial.stopReason = "aborted";
385
+ if (run.abortMessage) partial.errorMessage = run.abortMessage;
386
+ stream.push({ type: "error", reason: "aborted", error: partial });
329
387
  await cursorLiveRuns.release(run);
330
- return "chain_user_input";
388
+ outcome = "aborted";
389
+ outcomeDetails = { reason: "cancelled" };
390
+ return outcome;
331
391
  }
332
- turn.emitter.closeAll();
333
- const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
334
- if (finalText) {
335
- await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
392
+ if (run.errorMessage) {
393
+ partial.stopReason = "error";
394
+ partial.errorMessage = run.errorMessage;
395
+ stream.push({ type: "error", reason: "error", error: partial });
396
+ await cursorLiveRuns.release(run);
397
+ outcome = "error";
398
+ return outcome;
399
+ }
400
+ if (run.done) {
401
+ if (options.mode === "chain_user_input") {
402
+ await cursorLiveRuns.release(run);
403
+ outcome = "chain_user_input";
404
+ outcomeDetails = { reason: "run_done" };
405
+ return outcome;
406
+ }
407
+ turn.emitter.closeAll();
408
+ const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
409
+ if (finalText) {
410
+ await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
411
+ }
412
+ applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
413
+ partial.stopReason = "stop";
414
+ stream.push({ type: "done", reason: "stop", message: partial });
415
+ await cursorLiveRuns.release(run);
416
+ outcome = "stop";
417
+ outcomeDetails = { finalTextLength: finalText.length };
418
+ return outcome;
336
419
  }
337
- applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
338
- partial.stopReason = "stop";
339
- stream.push({ type: "done", reason: "stop", message: partial });
340
- await cursorLiveRuns.release(run);
341
- return "stop";
342
- }
343
420
 
344
- await cursorLiveRuns.waitForProgress(run, options.signal);
421
+ await cursorLiveRuns.waitForProgress(run, options.signal);
422
+ }
423
+ } catch (error) {
424
+ if (!outcome) {
425
+ if (error instanceof CursorLiveRunAbortError) {
426
+ outcome = "aborted";
427
+ outcomeDetails = { reason: "signal_aborted" };
428
+ } else {
429
+ outcome = "error";
430
+ outcomeDetails = {
431
+ reason: "drain_error",
432
+ errorMessage: error instanceof Error ? error.message : String(error),
433
+ };
434
+ }
435
+ }
436
+ throw error;
437
+ } finally {
438
+ debugRecorder?.recordDrainEvent("turn_end", {
439
+ outcome: outcome ?? "error",
440
+ runId: run.id,
441
+ pendingEventCount: run.pendingEvents.length,
442
+ done: run.done,
443
+ ...outcomeDetails,
444
+ });
345
445
  }
346
446
  }
347
447
 
@@ -351,10 +451,15 @@ export async function drainExistingCursorLiveRunBeforeSend(
351
451
  model: Model<Api>,
352
452
  context: Context,
353
453
  signal?: AbortSignal,
454
+ turnDebugRecorder?: CursorSdkEventDebugRecorder,
354
455
  ): Promise<LiveRunPreSendOutcome> {
456
+ turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
355
457
  while (true) {
356
458
  const run = getPendingCursorLiveRun(context) ?? getActiveCursorLiveRunForCurrentScope();
357
- if (!run || run.disposed) return "continue_send";
459
+ if (!run || run.disposed) {
460
+ turnDebugRecorder?.recordDrainEvent("pre_send_end", { outcome: "continue_send", reason: "no_pending_run" });
461
+ return "continue_send";
462
+ }
358
463
 
359
464
  try {
360
465
  const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
@@ -370,14 +475,28 @@ export async function drainExistingCursorLiveRunBeforeSend(
370
475
  const drainOutcome = await drainCursorLiveRunTurn(stream, partial, model, context, run, consumed.toolResultInputTokens, {
371
476
  mode: shouldChainUserInput ? "chain_user_input" : "emit",
372
477
  signal,
478
+ debugRecorder: turnDebugRecorder,
373
479
  });
374
- return drainOutcome === "chain_user_input" ? "continue_send" : "stream_ended";
480
+ const mapped = drainOutcome === "chain_user_input" ? "continue_send" : "stream_ended";
481
+ turnDebugRecorder?.recordDrainEvent("pre_send_iteration", {
482
+ runId: run.id,
483
+ drainOutcome,
484
+ outcome: mapped,
485
+ shouldChainUserInput,
486
+ });
487
+ return mapped;
375
488
  });
376
489
  if (outcome === "continue_send" && !run.disposed && cursorLiveRuns.getActiveForScope(run.sessionAgentScopeKey) === run) {
377
490
  continue;
378
491
  }
492
+ turnDebugRecorder?.recordDrainEvent("pre_send_end", { outcome, runId: run.id });
379
493
  return outcome;
380
494
  } catch (error) {
495
+ turnDebugRecorder?.recordDrainEvent("pre_send_end", {
496
+ outcome: error instanceof CursorLiveRunAbortError ? "aborted" : "error",
497
+ runId: run.id,
498
+ reason: error instanceof CursorLiveRunAbortError ? "signal_aborted" : "drain_error",
499
+ });
381
500
  if (error instanceof CursorLiveRunAbortError) await cursorLiveRuns.release(run);
382
501
  throw error;
383
502
  }