pi-cursor-sdk 0.1.17 → 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 (51) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +38 -1
  3. package/docs/cursor-live-smoke-checklist.md +22 -2
  4. package/docs/cursor-model-ux-spec.md +5 -4
  5. package/docs/cursor-native-tool-replay.md +96 -2
  6. package/docs/cursor-testing-lessons.md +428 -0
  7. package/package.json +11 -2
  8. package/scripts/debug-provider-events.mjs +403 -0
  9. package/scripts/debug-sdk-events.mjs +413 -0
  10. package/scripts/isolated-cursor-smoke.sh +226 -0
  11. package/scripts/lib/cursor-probe-utils.mjs +52 -0
  12. package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
  13. package/scripts/validate-smoke-jsonl.mjs +86 -7
  14. package/src/context.ts +45 -32
  15. package/src/cursor-agent-message-web-tools.ts +172 -0
  16. package/src/cursor-agents-context.ts +176 -0
  17. package/src/cursor-context-tools.ts +6 -0
  18. package/src/cursor-display-text.ts +10 -0
  19. package/src/cursor-incomplete-tool-visibility.ts +118 -0
  20. package/src/cursor-live-run-coordinator.ts +18 -7
  21. package/src/cursor-model.ts +12 -0
  22. package/src/cursor-native-replay-routing.ts +48 -0
  23. package/src/cursor-native-replay-trace.ts +29 -0
  24. package/src/cursor-native-tool-display-registration.ts +14 -7
  25. package/src/cursor-native-tool-display-replay.ts +63 -5
  26. package/src/cursor-native-tool-display-tools.ts +20 -0
  27. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  28. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  29. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  30. package/src/cursor-provider-errors.ts +96 -0
  31. package/src/cursor-provider-live-run-drain.ts +208 -63
  32. package/src/cursor-provider-turn-coordinator.ts +217 -47
  33. package/src/cursor-provider.ts +275 -83
  34. package/src/cursor-question-tool.ts +10 -5
  35. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  36. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  37. package/src/cursor-sdk-event-debug-session.ts +163 -0
  38. package/src/cursor-sdk-event-debug.ts +597 -0
  39. package/src/cursor-sensitive-text.ts +27 -7
  40. package/src/cursor-session-agent.ts +25 -3
  41. package/src/cursor-session-send-policy.ts +43 -0
  42. package/src/cursor-setting-sources.ts +29 -0
  43. package/src/cursor-state.ts +1 -5
  44. package/src/cursor-tool-lifecycle.ts +111 -0
  45. package/src/cursor-tool-names.ts +12 -0
  46. package/src/cursor-tool-transcript.ts +4 -2
  47. package/src/cursor-transcript-tool-formatters.ts +228 -5
  48. package/src/cursor-transcript-tool-specs.ts +113 -14
  49. package/src/cursor-transcript-utils.ts +12 -0
  50. package/src/cursor-web-tool-activity.ts +84 -0
  51. package/src/index.ts +4 -1
@@ -21,6 +21,7 @@ import {
21
21
  createCursorReplayOnlyToolDefinition,
22
22
  renderCursorReplayResult,
23
23
  renderNativeLookingCursorFileMutationCall,
24
+ renderNativeLookingCursorReadReplayResult,
24
25
  } from "./cursor-native-tool-display-replay.js";
25
26
  import {
26
27
  consumeCursorNativeToolDisplay,
@@ -65,6 +66,20 @@ export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
65
66
  return getCurrentDefinition().execute(toolCallId, params, signal, onUpdate, ctx);
66
67
  },
67
68
  renderCall(args, theme, context) {
69
+ if (definition.name === "read" && isCursorReplayToolCallId(context.toolCallId)) {
70
+ const currentRenderCall = getCurrentDefinition().renderCall;
71
+ const rendered = currentRenderCall ? currentRenderCall(args, theme, context) : new Text("", 0, 0);
72
+ if ((args as Record<string, unknown>).localReadPreview === true && !context.expanded) {
73
+ const baseText = rendered.render(120).join("\n").trimEnd();
74
+ const labeled = `${baseText}${theme.fg("muted", " · local file preview")}`;
75
+ if (rendered instanceof Text) {
76
+ rendered.setText(labeled);
77
+ return rendered;
78
+ }
79
+ return new Text(labeled, 0, 0);
80
+ }
81
+ return rendered;
82
+ }
68
83
  if (isCursorFileMutationToolName(definition.name) && isCursorReplayToolCallId(context.toolCallId)) {
69
84
  return renderNativeLookingCursorFileMutationCall(definition.name, args as Record<string, unknown>, theme, context.isPartial);
70
85
  }
@@ -76,6 +91,11 @@ export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
76
91
  if (isCursorFileMutationToolName(definition.name) && details?.cursorToolName === definition.name) {
77
92
  return renderCursorReplayResult(result, options, theme, context, context.isError);
78
93
  }
94
+ if (definition.name === "read" && isCursorReplayToolCallId(context.toolCallId)) {
95
+ return renderNativeLookingCursorReadReplayResult(result, options, theme, context, () =>
96
+ getCurrentDefinition().renderResult?.(result, options, theme, context),
97
+ );
98
+ }
79
99
  const currentRenderResult = getCurrentDefinition().renderResult;
80
100
  return currentRenderResult ? currentRenderResult(result, options, theme, context) : new Text("", 0, 0);
81
101
  },
@@ -1,5 +1,6 @@
1
1
  import { stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
2
2
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
3
+ import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
3
4
 
4
5
  export const CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG";
5
6
  export const CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX = "[pi-cursor-sdk:bridge]";
@@ -169,7 +170,16 @@ export function serializeCursorPiToolBridgeDiagnostic(event: CursorPiToolBridgeD
169
170
  return assertNeverDiagnosticEvent(event);
170
171
  }
171
172
 
172
- export function writeCursorPiToolBridgeDiagnostic(env: Record<string, string | undefined>, event: CursorPiToolBridgeDiagnosticEvent): void {
173
+ export function writeCursorPiToolBridgeDiagnostic(
174
+ env: Record<string, string | undefined>,
175
+ event: CursorPiToolBridgeDiagnosticEvent,
176
+ debugRecorder?: CursorSdkEventDebugRecorder,
177
+ ): void {
178
+ try {
179
+ debugRecorder?.recordBridgeDiagnostic(event);
180
+ } catch {
181
+ // Diagnostics must never affect bridge execution.
182
+ }
173
183
  if (!resolveCursorPiToolBridgeDebugEnabled(env)) return;
174
184
  try {
175
185
  process.stderr.write(`${CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX} ${JSON.stringify(serializeCursorPiToolBridgeDiagnostic(event))}\n`);
@@ -67,6 +67,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
67
67
  private readonly pendingByBridgeCallId = new Map<string, PendingBridgeCall>();
68
68
  private readonly pendingByCursorMcpCallId = new Map<string, PendingBridgeCall>();
69
69
  private onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
70
+ private debugRecorder: CursorPiToolBridgeRunOptions["debugRecorder"];
70
71
  private liveRunHandlerDetached = false;
71
72
  private mcpServer?: McpProtocolServer;
72
73
  private mcpTransport?: StreamableHTTPServerTransport;
@@ -85,6 +86,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
85
86
  this.snapshot = snapshot;
86
87
  this.enabled = enabled;
87
88
  this.onToolRequest = options.onToolRequest;
89
+ this.debugRecorder = options.debugRecorder;
88
90
  this.id = `cursor-pi-bridge-run-${randomUUID()}`;
89
91
  this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
90
92
  this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
@@ -146,6 +148,10 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
146
148
  }
147
149
  }
148
150
 
151
+ setDebugRecorder(recorder?: CursorPiToolBridgeRunOptions["debugRecorder"]): void {
152
+ this.debugRecorder = recorder;
153
+ }
154
+
149
155
  resolveToolResults(toolResults: readonly ToolResultMessage[]): void {
150
156
  for (const toolResult of toolResults) {
151
157
  const pending = this.pendingByPiToolCallId.get(toolResult.toolCallId);
@@ -290,9 +296,11 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
290
296
  }
291
297
  this.queuedRequests.push(request);
292
298
  this.emitRequestQueuedDiagnostic(request);
299
+ this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
293
300
  return;
294
301
  }
295
302
  this.emitRequestQueuedDiagnostic(request);
303
+ this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
296
304
  this.dispatchPendingToolRequest(pending, this.onToolRequest);
297
305
  });
298
306
  }
@@ -321,6 +329,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
321
329
  pending.settled = true;
322
330
  this.removePending(pending);
323
331
  this.emitRequestResolvedDiagnostic(pending.request, result.isError === true);
332
+ this.debugRecorder?.recordBridgeRaw({ kind: "resolved", request: pending.request, result });
324
333
  pending.resolve(result);
325
334
  }
326
335
 
@@ -329,6 +338,12 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
329
338
  pending.settled = true;
330
339
  this.removePending(pending);
331
340
  this.emitRequestRejectedDiagnostic(pending.request, kind);
341
+ this.debugRecorder?.recordBridgeRaw({
342
+ kind: "rejected",
343
+ request: pending.request,
344
+ error: error.message,
345
+ rejectionKind: kind,
346
+ });
332
347
  pending.reject(error);
333
348
  }
334
349
 
@@ -366,7 +381,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
366
381
  }
367
382
 
368
383
  private emitDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): void {
369
- writeCursorPiToolBridgeDiagnostic(this.env, event);
384
+ writeCursorPiToolBridgeDiagnostic(this.env, event, this.debugRecorder);
370
385
  }
371
386
 
372
387
  private pendingCount(): number {
@@ -1,5 +1,6 @@
1
1
  import type { McpServerConfig } from "@cursor/sdk";
2
2
  import type { Context, ToolResultMessage } from "@earendil-works/pi-ai";
3
+ import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
3
4
  import type {
4
5
  ExtensionAPI,
5
6
  ExtensionHandler,
@@ -64,6 +65,7 @@ export interface CursorPiToolBridgeRun {
64
65
  hasPendingPiToolCallId(piToolCallId: string): boolean;
65
66
  isBridgeMcpToolCall(toolCall: unknown): boolean;
66
67
  setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void;
68
+ setDebugRecorder(recorder?: CursorSdkEventDebugRecorder): void;
67
69
  cancel(reason: string): void;
68
70
  dispose(): Promise<void>;
69
71
  }
@@ -77,4 +79,5 @@ export interface CursorPiToolBridge {
77
79
 
78
80
  export interface CursorPiToolBridgeRunOptions {
79
81
  onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
82
+ debugRecorder?: CursorSdkEventDebugRecorder;
80
83
  }
@@ -0,0 +1,96 @@
1
+ import type { RunResult } from "@cursor/sdk";
2
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
+
4
+ export const MISSING_CURSOR_API_KEY_MESSAGE =
5
+ "Cursor SDK runs require a Cursor API key. Run /login -> Use an API key -> Cursor, set CURSOR_API_KEY before starting pi, or restart pi with --api-key.";
6
+ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
7
+ "Cursor SDK request failed. The API key may be missing, invalid, or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
8
+ const AUTH_CURSOR_SDK_ERROR_MESSAGE =
9
+ "Cursor SDK request failed because the API key may be invalid or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
10
+ const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
11
+ "Cursor SDK request timed out during network I/O. Check your connection and retry; if this keeps happening, try again later or verify Cursor service availability.";
12
+
13
+ const GENERIC_CURSOR_RUN_FAILURE_TEXT = "cursor sdk run failed";
14
+
15
+ export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "status" | "durationMs" | "model" | "result">;
16
+
17
+ function isGenericErrorMessage(message: string): boolean {
18
+ const normalized = message.trim().toLowerCase();
19
+ return normalized === "" || normalized === "error" || normalized === "unknown error";
20
+ }
21
+
22
+ function isKnownGenericRunFailureText(message: string): boolean {
23
+ const normalized = message.trim().toLowerCase();
24
+ return normalized === "" || normalized === GENERIC_CURSOR_RUN_FAILURE_TEXT || isGenericErrorMessage(normalized);
25
+ }
26
+
27
+ function isLikelyAuthError(message: string): boolean {
28
+ return /\b(unauthorized|unauthorised|forbidden|invalid api key|invalid key|authentication|auth|401|403)\b/i.test(message);
29
+ }
30
+
31
+ function isLikelyNetworkTimeout(message: string): boolean {
32
+ return (
33
+ /\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN)\b/i.test(message) ||
34
+ /\bConnectError\b.*\b(unavailable|deadline|timeout|timed out)\b/i.test(message) ||
35
+ /\bread ETIMEDOUT\b/i.test(message)
36
+ );
37
+ }
38
+
39
+ function shortRunId(runId: string): string {
40
+ const trimmed = runId.trim();
41
+ if (trimmed.length <= 12) return trimmed;
42
+ return `${trimmed.slice(0, 8)}…`;
43
+ }
44
+
45
+ export function formatCursorSdkRunFailureDetail(result: CursorSdkRunFailureSource, runResult?: string): string {
46
+ const fromWait = result.result?.trim();
47
+ if (fromWait && !isKnownGenericRunFailureText(fromWait)) {
48
+ return fromWait;
49
+ }
50
+ const fromRun = runResult?.trim();
51
+ if (fromRun && !isKnownGenericRunFailureText(fromRun)) {
52
+ return fromRun;
53
+ }
54
+
55
+ const parts = ["Cursor SDK run failed"];
56
+ if (result.model?.id) parts.push(`model ${result.model.id}`);
57
+ parts.push(`run ${shortRunId(result.id)}`);
58
+ if (typeof result.durationMs === "number") parts.push(`${result.durationMs}ms`);
59
+ return parts.join(" · ");
60
+ }
61
+
62
+ export type CursorSdkAbortCause = "user_interrupt" | "sdk_cancelled" | "live_run_disposed" | "unknown";
63
+
64
+ export function formatCursorSdkAbortMessage(cause: CursorSdkAbortCause): string {
65
+ switch (cause) {
66
+ case "user_interrupt":
67
+ return "Cancelled: prompt interrupted.";
68
+ case "sdk_cancelled":
69
+ return "Cancelled: Cursor SDK run was cancelled.";
70
+ case "live_run_disposed":
71
+ return "Cancelled: Cursor SDK live run ended before completion.";
72
+ case "unknown":
73
+ return "Cancelled: Cursor SDK run aborted.";
74
+ }
75
+ }
76
+
77
+ export function resolveCursorSdkAbortCause(options: {
78
+ signalAborted?: boolean;
79
+ sdkStatusCancelled?: boolean;
80
+ liveRunDisposed?: boolean;
81
+ }): CursorSdkAbortCause {
82
+ if (options.signalAborted) return "user_interrupt";
83
+ if (options.sdkStatusCancelled) return "sdk_cancelled";
84
+ if (options.liveRunDisposed) return "live_run_disposed";
85
+ return "unknown";
86
+ }
87
+
88
+ export function sanitizeCursorProviderError(error: unknown, apiKey?: string): string {
89
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
90
+ if (message === MISSING_CURSOR_API_KEY_MESSAGE) return MISSING_CURSOR_API_KEY_MESSAGE;
91
+ const scrubbed = scrubSensitiveText(message, apiKey).trim();
92
+ if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
93
+ if (isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
94
+ if (isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
95
+ return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
96
+ }
@@ -23,6 +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";
27
+ import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
28
+ import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
29
+ import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
26
30
 
27
31
  export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
28
32
  const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
@@ -101,6 +105,37 @@ export async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<voi
101
105
  await scheduler.wait(75);
102
106
  }
103
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
+
104
139
  function emitCursorLiveQueuedEvent(
105
140
  turn: CursorLiveTurnState,
106
141
  event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
@@ -176,6 +211,7 @@ function emitCursorNativeToolUseTurn(
176
211
  run: CursorLiveRun,
177
212
  toolResultInputTokens: number,
178
213
  tools: CursorNativeToolDisplayItem[],
214
+ debugRecorder?: CursorSdkEventDebugRecorder,
179
215
  ): void {
180
216
  const shouldTerminate = run.done && !run.finalText?.trim() && !cursorLiveRuns.peekEvent(run);
181
217
  for (const tool of tools) {
@@ -192,6 +228,11 @@ function emitCursorNativeToolUseTurn(
192
228
  if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
193
229
  if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
194
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
+ });
195
236
  }
196
237
  }
197
238
  applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
@@ -200,6 +241,23 @@ function emitCursorNativeToolUseTurn(
200
241
  cursorLiveRuns.requestIdleDispose(run);
201
242
  }
202
243
 
244
+ function emitInactiveCursorReplayTrace(
245
+ turn: CursorLiveTurnState,
246
+ tools: CursorNativeToolDisplayItem[],
247
+ debugRecorder?: CursorSdkEventDebugRecorder,
248
+ ): void {
249
+ if (tools.length === 0) return;
250
+ for (const tool of tools) {
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);
258
+ }
259
+ }
260
+
203
261
  function emitCursorBridgeToolUseTurn(
204
262
  stream: AssistantMessageEventStream,
205
263
  partial: AssistantMessage,
@@ -229,24 +287,31 @@ function emitCursorBridgeToolUseTurn(
229
287
  }
230
288
 
231
289
  async function emitCursorLiveRunPendingToolUseTurn(
290
+ turn: CursorLiveTurnState,
232
291
  stream: AssistantMessageEventStream,
233
292
  partial: AssistantMessage,
234
293
  model: Model<Api>,
235
294
  context: Context,
236
295
  run: CursorLiveRun,
237
296
  toolResultInputTokens: number,
238
- signal?: AbortSignal,
239
- beforeEmit?: () => void,
240
- ): Promise<"tool_use" | undefined> {
297
+ options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
298
+ ): Promise<"tool_use" | "handled" | undefined> {
299
+ const debugRecorder = options.debugRecorder ?? run.debugRecorder;
241
300
  const eventType = cursorLiveRuns.peekEvent(run)?.type;
242
301
  if (eventType !== "tool" && eventType !== "bridge-tool") return undefined;
243
302
  await settleCursorLiveToolBatch(run);
244
- if (signal?.aborted) throw new CursorLiveRunAbortError();
245
- beforeEmit?.();
303
+ if (options.signal?.aborted) throw new CursorLiveRunAbortError();
246
304
  if (eventType === "tool") {
247
- const tools = cursorLiveRuns.collectNativeToolBatch(run);
248
- emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, tools);
305
+ const { active, inactive } = partitionNativeToolsByActiveContext(context, cursorLiveRuns.collectNativeToolBatch(run));
306
+ if (options.mode === "emit") emitInactiveCursorReplayTrace(turn, inactive, debugRecorder);
307
+ if (active.length === 0) {
308
+ // Inactive-only batch: trace was emitted above; do not emit toolUse.
309
+ return "handled";
310
+ }
311
+ if (options.mode === "emit") turn.emitter.closeAll();
312
+ emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active, debugRecorder);
249
313
  } else {
314
+ if (options.mode === "emit") turn.emitter.closeAll();
250
315
  const requests = cursorLiveRuns.collectBridgeToolBatch(run);
251
316
  emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
252
317
  }
@@ -260,72 +325,123 @@ export async function drainCursorLiveRunTurn(
260
325
  context: Context,
261
326
  run: CursorLiveRun,
262
327
  toolResultInputTokens: number,
263
- options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal },
328
+ options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
264
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> = {};
265
339
  const turn: CursorLiveTurnState = {
266
340
  emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
267
341
  emittedText: "",
268
342
  };
269
343
 
270
- while (true) {
271
- if (options.mode === "chain_user_input" && cursorLiveRuns.isReady(run)) {
272
- await cursorLiveRuns.release(run);
273
- return "chain_user_input";
274
- }
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
+ }
275
351
 
276
- while (cursorLiveRuns.peekEvent(run)) {
277
- const toolUse = await emitCursorLiveRunPendingToolUseTurn(
278
- stream,
279
- partial,
280
- model,
281
- context,
282
- run,
283
- toolResultInputTokens,
284
- options.signal,
285
- options.mode === "emit" ? () => turn.emitter.closeAll() : undefined,
286
- );
287
- if (toolUse) return toolUse;
288
- const event = cursorLiveRuns.shiftEvent(run);
289
- if (!event || event.type === "tool" || event.type === "bridge-tool") continue;
290
- if (options.mode === "emit") emitCursorLiveQueuedEvent(turn, event, run);
291
- }
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
+ }
292
372
 
293
- if (run.disposed) {
294
- partial.stopReason = "aborted";
295
- stream.push({ type: "error", reason: "aborted", error: partial });
296
- return "aborted";
297
- }
298
- if (run.cancelled) {
299
- partial.stopReason = "aborted";
300
- stream.push({ type: "error", reason: "aborted", error: partial });
301
- await cursorLiveRuns.release(run);
302
- return "aborted";
303
- }
304
- if (run.errorMessage) {
305
- partial.stopReason = "error";
306
- partial.errorMessage = run.errorMessage;
307
- stream.push({ type: "error", reason: "error", error: partial });
308
- await cursorLiveRuns.release(run);
309
- return "error";
310
- }
311
- if (run.done) {
312
- 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 });
313
387
  await cursorLiveRuns.release(run);
314
- return "chain_user_input";
388
+ outcome = "aborted";
389
+ outcomeDetails = { reason: "cancelled" };
390
+ return outcome;
315
391
  }
316
- turn.emitter.closeAll();
317
- const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
318
- if (finalText) {
319
- 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;
320
419
  }
321
- applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
322
- partial.stopReason = "stop";
323
- stream.push({ type: "done", reason: "stop", message: partial });
324
- await cursorLiveRuns.release(run);
325
- return "stop";
326
- }
327
420
 
328
- 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
+ });
329
445
  }
330
446
  }
331
447
 
@@ -335,10 +451,15 @@ export async function drainExistingCursorLiveRunBeforeSend(
335
451
  model: Model<Api>,
336
452
  context: Context,
337
453
  signal?: AbortSignal,
454
+ turnDebugRecorder?: CursorSdkEventDebugRecorder,
338
455
  ): Promise<LiveRunPreSendOutcome> {
456
+ turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
339
457
  while (true) {
340
458
  const run = getPendingCursorLiveRun(context) ?? getActiveCursorLiveRunForCurrentScope();
341
- 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
+ }
342
463
 
343
464
  try {
344
465
  const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
@@ -354,14 +475,28 @@ export async function drainExistingCursorLiveRunBeforeSend(
354
475
  const drainOutcome = await drainCursorLiveRunTurn(stream, partial, model, context, run, consumed.toolResultInputTokens, {
355
476
  mode: shouldChainUserInput ? "chain_user_input" : "emit",
356
477
  signal,
478
+ debugRecorder: turnDebugRecorder,
479
+ });
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,
357
486
  });
358
- return drainOutcome === "chain_user_input" ? "continue_send" : "stream_ended";
487
+ return mapped;
359
488
  });
360
489
  if (outcome === "continue_send" && !run.disposed && cursorLiveRuns.getActiveForScope(run.sessionAgentScopeKey) === run) {
361
490
  continue;
362
491
  }
492
+ turnDebugRecorder?.recordDrainEvent("pre_send_end", { outcome, runId: run.id });
363
493
  return outcome;
364
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
+ });
365
500
  if (error instanceof CursorLiveRunAbortError) await cursorLiveRuns.release(run);
366
501
  throw error;
367
502
  }
@@ -376,4 +511,14 @@ export function resetCursorNativeReplayIdleDisposeMs(): void {
376
511
  cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
377
512
  }
378
513
 
514
+ export async function releaseAllPendingCursorLiveRunsForTests(): Promise<void> {
515
+ while (cursorLiveRuns.count() > 0) {
516
+ const run = cursorLiveRuns.getActiveForScope();
517
+ if (!run) break;
518
+ const before = cursorLiveRuns.count();
519
+ await cursorLiveRuns.release(run);
520
+ if (cursorLiveRuns.count() >= before) break;
521
+ }
522
+ }
523
+
379
524
  export { hasTrailingUserMessagesAfterToolResults };