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
@@ -8,14 +8,15 @@ import {
8
8
  type AssistantMessage,
9
9
  } from "@earendil-works/pi-ai";
10
10
  import { Agent, createAgentPlatform } from "@cursor/sdk";
11
- import type { SDKAgent, SettingSource } from "@cursor/sdk";
11
+ import type { SDKAgent } from "@cursor/sdk";
12
12
  import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
13
13
  import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
14
- import { buildCursorSendPrompt } from "./context.js";
15
14
  import {
16
15
  acquireSessionCursorAgent,
16
+ buildCursorSessionSendPrompt,
17
17
  commitSessionAgentSend,
18
18
  disposeAllSessionCursorAgents,
19
+ planCursorSessionSend,
19
20
  resetSessionCursorAgent,
20
21
  } from "./cursor-session-agent.js";
21
22
  import {
@@ -28,6 +29,7 @@ import {
28
29
  getCursorPromptOptions,
29
30
  } from "./cursor-usage-accounting.js";
30
31
  import { getCursorSessionCwd } from "./cursor-session-cwd.js";
32
+ import { getActiveContextToolNames } from "./cursor-context-tools.js";
31
33
  import { CursorLiveRunAbortError, type CursorLiveRun } from "./cursor-live-run-coordinator.js";
32
34
  import {
33
35
  abandonSessionCursorAgent,
@@ -35,9 +37,11 @@ import {
35
37
  cursorLiveRuns,
36
38
  drainCursorLiveRunTurn,
37
39
  drainExistingCursorLiveRunBeforeSend,
40
+ flushPendingCursorLiveRunTraceEventsToStream,
38
41
  DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
39
42
  getPendingCursorLiveRun,
40
43
  hasTrailingUserMessagesAfterToolResults,
44
+ releaseAllPendingCursorLiveRunsForTests,
41
45
  resetCursorNativeReplayIdleDisposeMs,
42
46
  selectCursorFinalText,
43
47
  setCursorNativeReplayIdleDisposeMs,
@@ -46,8 +50,27 @@ import {
46
50
  import { getEffectiveFastForModelId } from "./cursor-state.js";
47
51
  import { buildCursorModelSelection } from "./model-discovery.js";
48
52
  import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
53
+ import {
54
+ attachCursorSdkEventDebugPiStreamTap,
55
+ CursorSdkEventDebugSink,
56
+ DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
57
+ } from "./cursor-sdk-event-debug.js";
49
58
  import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
50
59
  import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display.js";
60
+ import {
61
+ formatCursorSdkAbortMessage,
62
+ formatCursorSdkRunFailureDetail,
63
+ MISSING_CURSOR_API_KEY_MESSAGE,
64
+ resolveCursorSdkAbortCause,
65
+ sanitizeCursorProviderError,
66
+ } from "./cursor-provider-errors.js";
67
+ import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
68
+ import { hasUsableText } from "./cursor-record-utils.js";
69
+ import {
70
+ countCursorAgentMessages,
71
+ loadCursorTranscriptWebToolCallsAfterOffset,
72
+ } from "./cursor-agent-message-web-tools.js";
73
+ import { installCursorSdkAbortErrorSuppression } from "./cursor-sdk-abort-error-guard.js";
51
74
 
52
75
  function makeInitialMessage(model: Model<Api>): AssistantMessage {
53
76
  return {
@@ -70,25 +93,6 @@ function makeInitialMessage(model: Model<Api>): AssistantMessage {
70
93
  }
71
94
 
72
95
  const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
73
- const MISSING_API_KEY_MESSAGE =
74
- "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.";
75
- const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
76
- "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.";
77
- const AUTH_CURSOR_SDK_ERROR_MESSAGE =
78
- "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.";
79
- const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
80
-
81
- import { scrubSensitiveText } from "./cursor-sensitive-text.js";
82
- import { hasUsableText } from "./cursor-record-utils.js";
83
-
84
- function isGenericErrorMessage(message: string): boolean {
85
- const normalized = message.trim().toLowerCase();
86
- return normalized === "" || normalized === "error" || normalized === "unknown error";
87
- }
88
-
89
- function isLikelyAuthError(message: string): boolean {
90
- return /\b(unauthorized|unauthorised|forbidden|invalid api key|invalid key|authentication|auth|401|403)\b/i.test(message);
91
- }
92
96
 
93
97
  function resolveCursorApiKey(apiKey?: string): string | undefined {
94
98
  const trimmed = apiKey?.trim();
@@ -97,27 +101,6 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
97
101
  return trimmed;
98
102
  }
99
103
 
100
- function resolveCursorSettingSources(): SettingSource[] | undefined {
101
- const raw = process.env[CURSOR_SETTING_SOURCES_ENV]?.trim();
102
- if (!raw) return ["all"];
103
- const normalized = raw.toLowerCase();
104
- if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
105
- if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
106
- return raw
107
- .split(",")
108
- .map((entry) => entry.trim())
109
- .filter((entry): entry is SettingSource => Boolean(entry));
110
- }
111
-
112
- function sanitizeError(error: unknown, apiKey?: string): string {
113
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
114
- if (message === MISSING_API_KEY_MESSAGE) return MISSING_API_KEY_MESSAGE;
115
- const scrubbed = scrubSensitiveText(message, apiKey).trim();
116
- if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
117
- if (isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
118
- return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
119
- }
120
-
121
104
  async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<void> {
122
105
  try {
123
106
  const platform = await createAgentPlatform();
@@ -135,6 +118,8 @@ export function streamCursor(
135
118
  options?: SimpleStreamOptions,
136
119
  ): AssistantMessageEventStream {
137
120
  const stream = createAssistantMessageEventStream();
121
+ const sdkEventDebugRef: { current?: CursorSdkEventDebugSink } = {};
122
+ attachCursorSdkEventDebugPiStreamTap(stream, sdkEventDebugRef);
138
123
 
139
124
  (async () => {
140
125
  const partial = makeInitialMessage(model);
@@ -148,8 +133,51 @@ export function streamCursor(
148
133
  let abortSignal: AbortSignal | undefined;
149
134
  let abortListener: (() => void) | undefined;
150
135
  let restoreCursorSdkOutputFilter: (() => void) | undefined;
136
+ let sdkEventDebug: CursorSdkEventDebugSink | undefined;
137
+ let deferSdkEventDebugFinalize = false;
138
+ const sdkAbortErrorSuppression = installCursorSdkAbortErrorSuppression();
139
+
140
+ const pushSanitizedStreamError = (error: unknown, reason: "error" | "aborted" = "error"): void => {
141
+ partial.stopReason = reason;
142
+ partial.errorMessage =
143
+ reason === "aborted"
144
+ ? formatCursorSdkAbortMessage(resolveCursorSdkAbortCause({ signalAborted: options?.signal?.aborted }))
145
+ : sanitizeCursorProviderError(error, resolvedApiKey ?? options?.apiKey);
146
+ stream.push({ type: "error", reason, error: partial });
147
+ };
148
+
149
+ const getCursorAgentMessageOffset = async (agentId: string, cwd: string): Promise<number | undefined> => {
150
+ try {
151
+ return await countCursorAgentMessages(agentId, cwd);
152
+ } catch (error) {
153
+ sdkEventDebug?.recordError("cursor_agent_message_count", error);
154
+ return undefined;
155
+ }
156
+ };
157
+
158
+ const replayCursorTranscriptWebToolCalls = async (
159
+ agentId: string,
160
+ cwd: string,
161
+ messageOffset: number | undefined,
162
+ turnCoordinator: CursorSdkTurnCoordinator,
163
+ ): Promise<void> => {
164
+ try {
165
+ const transcriptToolCalls = await loadCursorTranscriptWebToolCallsAfterOffset({ agentId, cwd, offset: messageOffset });
166
+ if (transcriptToolCalls.length === 0) return;
167
+ sdkEventDebug?.recordCoordinatorEvent("cursor-transcript-web-tools", {
168
+ agentId,
169
+ messageOffset,
170
+ count: transcriptToolCalls.length,
171
+ });
172
+ turnCoordinator.handleTranscriptCompletedToolCalls(transcriptToolCalls);
173
+ } catch (error) {
174
+ sdkEventDebug?.recordError("cursor_transcript_web_tools", error);
175
+ }
176
+ };
151
177
 
152
178
  try {
179
+ let turnCoordinatorForCleanup: CursorSdkTurnCoordinator | undefined;
180
+ try {
153
181
  const throwIfAborted = (): void => {
154
182
  if (options?.signal?.aborted) throw new CursorLiveRunAbortError();
155
183
  };
@@ -157,21 +185,29 @@ export function streamCursor(
157
185
  stream.push({ type: "start", partial });
158
186
  throwIfAborted();
159
187
 
160
- if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal)) === "stream_ended") {
161
- stream.end();
188
+ const cwd = getCursorSessionCwd();
189
+ sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
190
+ cwd,
191
+ modelId: model.id,
192
+ provider: model.provider,
193
+ });
194
+ sdkEventDebugRef.current = sdkEventDebug;
195
+ sdkEventDebug?.recordContextSnapshot(context);
196
+
197
+ if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, sdkEventDebug)) === "stream_ended") {
198
+ sdkEventDebug?.recordFinalPartial(partial);
199
+ await sdkEventDebug?.finalize();
200
+ sdkEventDebugRef.current = undefined;
162
201
  return;
163
202
  }
164
203
 
165
204
  const apiKey = resolveCursorApiKey(options?.apiKey);
166
- if (!apiKey) throw new Error(MISSING_API_KEY_MESSAGE);
205
+ if (!apiKey) throw new Error(MISSING_CURSOR_API_KEY_MESSAGE);
167
206
  resolvedApiKey = apiKey;
168
207
 
169
- // pi-ai Context/SimpleStreamOptions do not expose ExtensionContext.cwd; bridge via session_start
170
- // until pi threads session cwd into streamSimple (cwd can change without a new session event).
171
- const cwd = getCursorSessionCwd();
172
208
  const fastEnabled = getEffectiveFastForModelId(model.id);
173
209
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
174
- const settingSources = resolveCursorSettingSources();
210
+ const settingSources = getEffectiveCursorSettingSources();
175
211
 
176
212
  installCursorMcpToolTimeoutOverride();
177
213
  restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
@@ -180,6 +216,7 @@ export function streamCursor(
180
216
  cwd,
181
217
  modelSelection: selection,
182
218
  settingSources,
219
+ debugRecorder: sdkEventDebug,
183
220
  onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
184
221
  if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
185
222
  cursorLiveRuns.queueEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
@@ -197,18 +234,39 @@ export function streamCursor(
197
234
  throwIfAborted();
198
235
 
199
236
  const promptOptions = getCursorPromptOptions(model);
200
- let { prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState);
201
- if (sessionAgentLease.sendState.bootstrapped && bootstrap) {
237
+ let sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
238
+ let prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
239
+ if (sendPlan.resetAgent) {
202
240
  await resetSessionCursorAgent(sessionAgentLease.scopeKey);
203
241
  sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
204
242
  sessionAgentScopeKey = sessionAgentLease.scopeKey;
205
243
  agent = sessionAgentLease.agent;
206
244
  bridgeRun = sessionAgentLease.bridgeRun;
207
- ({ prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState));
245
+ sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
246
+ prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
208
247
  }
248
+ const bootstrap = sendPlan.mode === "bootstrap";
209
249
  const sessionBridgeRun = sessionAgentLease.bridgeRun;
210
250
  const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
211
251
  const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
252
+ const activeToolNames = getActiveContextToolNames(context);
253
+ sdkEventDebug?.recordProviderMeta({
254
+ model: {
255
+ id: model.id,
256
+ provider: model.provider,
257
+ api: model.api,
258
+ reasoning: options?.reasoning ?? "off",
259
+ fastEnabled,
260
+ selection,
261
+ },
262
+ settingSources: settingSources ?? null,
263
+ sendState: sessionAgentLease.sendState,
264
+ sendPlan,
265
+ promptOptions,
266
+ activeToolNames: activeToolNames ? [...activeToolNames] : [],
267
+ sessionAgentScopeKey,
268
+ bridgeRunId: bridgeRun?.id,
269
+ });
212
270
  const nativeReplayId = createCursorNativeReplayId();
213
271
  const textDeltas: string[] = [];
214
272
  const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
@@ -221,6 +279,7 @@ export function streamCursor(
221
279
  sessionAgentScopeKey,
222
280
  promptInputTokens,
223
281
  textDeltas,
282
+ debugRecorder: sdkEventDebug,
224
283
  })
225
284
  : undefined;
226
285
  if (liveRun) {
@@ -237,13 +296,17 @@ export function streamCursor(
237
296
  resolvedApiKey,
238
297
  liveRun,
239
298
  useNativeToolReplay,
299
+ activeToolNames,
240
300
  nativeReplayId,
241
301
  textDeltas,
302
+ debugRecorder: sdkEventDebug,
242
303
  });
304
+ turnCoordinatorForCleanup = turnCoordinator;
243
305
 
244
306
  // Handle abort signal
245
307
  let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
246
308
  abortListener = () => {
309
+ sdkAbortErrorSuppression.suppressAbortErrors();
247
310
  activeLiveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
248
311
  if (run) {
249
312
  run.cancel().catch(() => {});
@@ -253,25 +316,73 @@ export function streamCursor(
253
316
  abortSignal?.addEventListener("abort", abortListener, { once: true });
254
317
 
255
318
  throwIfAborted();
256
- run = await agent.send(
257
- { text: prompt.text, images: prompt.images.length > 0 ? prompt.images : undefined },
258
- {
259
- onDelta: (args) => turnCoordinator.handleDelta(args.update),
260
- onStep: (args) => turnCoordinator.handleStep(args.step),
319
+ sdkEventDebug?.recordSendMeta({
320
+ mode: sendPlan.mode,
321
+ reason: sendPlan.reason,
322
+ resetAgent: sendPlan.resetAgent,
323
+ bootstrap,
324
+ promptText: prompt.text,
325
+ imageCount: prompt.images.length,
326
+ useNativeToolReplay,
327
+ bridgeEnabled: bridgeRun !== undefined,
328
+ nativeReplayId,
329
+ promptInputTokens,
330
+ });
331
+ const sendPayload = {
332
+ text: prompt.text,
333
+ images: prompt.images.length > 0 ? prompt.images : undefined,
334
+ };
335
+ sdkEventDebug?.recordSendPayload(sendPayload);
336
+ const cursorAgentMessageOffset = await getCursorAgentMessageOffset(agent.agentId, cwd);
337
+ sdkEventDebug?.recordProviderEvent("agent_send_start", sendPayload);
338
+ run = await agent.send(sendPayload, {
339
+ onDelta: (args) => {
340
+ sdkEventDebug?.recordOnDelta(args.update);
341
+ turnCoordinator.handleDelta(args.update);
261
342
  },
262
- );
343
+ onStep: (args) => {
344
+ sdkEventDebug?.recordOnStep(args.step);
345
+ turnCoordinator.handleStep(args.step);
346
+ },
347
+ });
348
+ sdkEventDebug?.recordRunMeta({
349
+ runId: run.id,
350
+ agentId: run.agentId,
351
+ status: run.status,
352
+ });
353
+ sdkEventDebug?.attachRunStream(run);
354
+ sdkEventDebug?.recordProviderEvent("agent_send_returned", {
355
+ runId: run.id,
356
+ agentId: run.agentId,
357
+ status: run.status,
358
+ });
263
359
  if (liveRun) cursorLiveRuns.attachSdkRun(liveRun, run);
264
360
  if (options?.signal?.aborted) {
361
+ sdkAbortErrorSuppression.suppressAbortErrors();
362
+ liveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
265
363
  await run.cancel().catch(() => {});
266
364
  throw new CursorLiveRunAbortError();
267
365
  }
366
+ const activeRun = run;
268
367
 
269
368
  if (liveRun) {
270
- void run
369
+ deferSdkEventDebugFinalize = true;
370
+ const waitCompletion = run
271
371
  .wait()
272
372
  .then(async (result) => {
373
+ sdkEventDebug?.recordWaitResult(result);
374
+ if (result.status === "finished" && !options?.signal?.aborted) {
375
+ await replayCursorTranscriptWebToolCalls(activeRun.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
376
+ }
377
+ turnCoordinator.discardIncompleteStartedToolCalls(
378
+ result.status === "cancelled" || options?.signal?.aborted
379
+ ? "abort"
380
+ : result.status === "error"
381
+ ? "sdk-failure"
382
+ : DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
383
+ );
384
+ await sdkEventDebug?.captureRunArtifacts(run);
273
385
  if (liveRun.disposed) return;
274
- turnCoordinator.discardIncompleteStartedToolCalls();
275
386
  await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
276
387
  if (liveRun.disposed) return;
277
388
  if (result.status === "finished" && !options?.signal?.aborted) {
@@ -281,14 +392,30 @@ export function streamCursor(
281
392
  selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, turnCoordinator.planTextCandidate),
282
393
  );
283
394
  } else if (result.status === "cancelled" || options?.signal?.aborted) {
284
- cursorLiveRuns.markCancelled(liveRun);
395
+ cursorLiveRuns.markCancelled(
396
+ liveRun,
397
+ formatCursorSdkAbortMessage(
398
+ resolveCursorSdkAbortCause({
399
+ signalAborted: options?.signal?.aborted,
400
+ sdkStatusCancelled: result.status === "cancelled",
401
+ }),
402
+ ),
403
+ );
285
404
  } else {
286
- cursorLiveRuns.markError(liveRun, sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey));
405
+ const failureDetail = formatCursorSdkRunFailureDetail(result, run?.result);
406
+ cursorLiveRuns.markError(
407
+ liveRun,
408
+ sanitizeCursorProviderError(failureDetail, resolvedApiKey ?? options?.apiKey),
409
+ );
287
410
  }
288
411
  })
289
412
  .catch(async (error: unknown) => {
413
+ sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) });
414
+ sdkEventDebug?.recordError("run_wait", error);
415
+ turnCoordinatorForCleanup?.discardIncompleteStartedToolCalls("sdk-failure");
416
+ await sdkEventDebug?.captureRunArtifacts(run);
290
417
  if (liveRun.disposed) return;
291
- cursorLiveRuns.markError(liveRun, sanitizeError(error, resolvedApiKey ?? options?.apiKey));
418
+ cursorLiveRuns.markError(liveRun, sanitizeCursorProviderError(error, resolvedApiKey ?? options?.apiKey));
292
419
  });
293
420
 
294
421
  try {
@@ -296,18 +423,53 @@ export function streamCursor(
296
423
  await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
297
424
  await settleCursorLiveToolBatch(liveRun);
298
425
  turnCoordinator.closeTraceBlock();
299
- await drainCursorLiveRunTurn(stream, partial, model, context, liveRun, 0, { mode: "emit", signal: options?.signal });
426
+ await drainCursorLiveRunTurn(stream, partial, model, context, liveRun, 0, {
427
+ mode: "emit",
428
+ signal: options?.signal,
429
+ debugRecorder: sdkEventDebug,
430
+ });
300
431
  });
301
432
  } catch (error) {
302
- if (error instanceof CursorLiveRunAbortError) await cursorLiveRuns.release(liveRun);
433
+ if (error instanceof CursorLiveRunAbortError) {
434
+ sdkAbortErrorSuppression.suppressAbortErrors();
435
+ turnCoordinator.discardIncompleteStartedToolCalls("abort");
436
+ turnCoordinator.closeTraceBlock();
437
+ flushPendingCursorLiveRunTraceEventsToStream(stream, partial, liveRun, {
438
+ includeTracesBehindQueuedTools: true,
439
+ });
440
+ await cursorLiveRuns.release(liveRun);
441
+ }
303
442
  throw error;
443
+ } finally {
444
+ sdkEventDebugRef.current = undefined;
445
+ void waitCompletion
446
+ .finally(async () => {
447
+ try {
448
+ sdkEventDebug?.recordFinalPartial(partial);
449
+ await sdkEventDebug?.finalize();
450
+ } finally {
451
+ sdkAbortErrorSuppression.dispose();
452
+ }
453
+ })
454
+ .catch(() => {});
304
455
  }
305
456
  agent = null;
306
457
  return;
307
458
  }
308
459
 
309
460
  const result = await run.wait();
310
- turnCoordinator.discardIncompleteStartedToolCalls();
461
+ sdkEventDebug?.recordWaitResult(result);
462
+ if (result.status === "finished" && !options?.signal?.aborted) {
463
+ await replayCursorTranscriptWebToolCalls(run.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
464
+ }
465
+ turnCoordinator.discardIncompleteStartedToolCalls(
466
+ result.status === "cancelled" || options?.signal?.aborted
467
+ ? "abort"
468
+ : result.status === "error"
469
+ ? "sdk-failure"
470
+ : DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
471
+ );
472
+ await sdkEventDebug?.captureRunArtifacts(run);
311
473
  await cacheSdkContextWindow(agent.agentId, model.id);
312
474
 
313
475
  // Close any open thinking/activity trace, then use the final run result only when
@@ -317,11 +479,18 @@ export function streamCursor(
317
479
  if (result.status === "cancelled") {
318
480
  await abandonSessionCursorAgent(sessionAgentScopeKey);
319
481
  partial.stopReason = "aborted";
482
+ partial.errorMessage = formatCursorSdkAbortMessage(
483
+ resolveCursorSdkAbortCause({
484
+ signalAborted: options?.signal?.aborted,
485
+ sdkStatusCancelled: true,
486
+ }),
487
+ );
320
488
  stream.push({ type: "error", reason: "aborted", error: partial });
321
489
  } else if (result.status === "error") {
322
490
  await abandonSessionCursorAgent(sessionAgentScopeKey);
323
491
  partial.stopReason = "error";
324
- partial.errorMessage = sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey);
492
+ const failureDetail = formatCursorSdkRunFailureDetail(result, run.result);
493
+ partial.errorMessage = sanitizeCursorProviderError(failureDetail, resolvedApiKey ?? options?.apiKey);
325
494
  stream.push({ type: "error", reason: "error", error: partial });
326
495
  } else {
327
496
  commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
@@ -332,27 +501,49 @@ export function streamCursor(
332
501
  applyCursorApproximateUsage(partial, model, context, promptInputTokens);
333
502
  stream.push({ type: "done", reason: "stop", message: partial });
334
503
  }
335
- } catch (error) {
336
- if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun);
337
- else await abandonSessionCursorAgent(sessionAgentScopeKey);
338
- if (error instanceof CursorLiveRunAbortError) {
339
- partial.stopReason = "aborted";
340
- stream.push({ type: "error", reason: "aborted", error: partial });
341
- } else {
342
- partial.stopReason = "error";
343
- partial.errorMessage = sanitizeError(error, resolvedApiKey ?? options?.apiKey);
344
- stream.push({ type: "error", reason: "error", error: partial });
345
- }
346
- } finally {
347
- restoreCursorSdkOutputFilter?.();
504
+ } catch (error) {
505
+ sdkEventDebug?.recordError("provider_stream", error);
506
+ turnCoordinatorForCleanup?.discardIncompleteStartedToolCalls(
507
+ error instanceof CursorLiveRunAbortError ? "abort" : "sdk-failure",
508
+ );
509
+ if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun);
510
+ else await abandonSessionCursorAgent(sessionAgentScopeKey);
511
+ if (error instanceof CursorLiveRunAbortError) {
512
+ sdkAbortErrorSuppression.suppressAbortErrors();
513
+ pushSanitizedStreamError(error, "aborted");
514
+ } else {
515
+ pushSanitizedStreamError(error, "error");
516
+ }
517
+ } finally {
518
+ if (!deferSdkEventDebugFinalize) {
519
+ try {
520
+ sdkEventDebug?.recordFinalPartial(partial);
521
+ await sdkEventDebug?.finalize();
522
+ } finally {
523
+ sdkAbortErrorSuppression.dispose();
524
+ }
525
+ }
526
+ sdkEventDebugRef.current = undefined;
527
+ restoreCursorSdkOutputFilter?.();
348
528
 
349
- if (abortSignal && abortListener) {
350
- abortSignal.removeEventListener("abort", abortListener);
529
+ if (abortSignal && abortListener) {
530
+ abortSignal.removeEventListener("abort", abortListener);
531
+ }
351
532
  }
533
+ } catch (error) {
534
+ if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun).catch(() => {});
535
+ else await abandonSessionCursorAgent(sessionAgentScopeKey).catch(() => {});
536
+ pushSanitizedStreamError(error, error instanceof CursorLiveRunAbortError ? "aborted" : "error");
352
537
  }
353
538
 
354
539
  stream.end();
355
- })();
540
+ })().catch((error: unknown) => {
541
+ const partial = makeInitialMessage(model);
542
+ partial.stopReason = "error";
543
+ partial.errorMessage = sanitizeCursorProviderError(error, resolveCursorApiKey(options?.apiKey));
544
+ stream.push({ type: "error", reason: "error", error: partial });
545
+ stream.end();
546
+ });
356
547
 
357
548
  return stream;
358
549
  }
@@ -365,5 +556,6 @@ export const __testUtils = {
365
556
  hasTrailingUserMessagesAfterToolResults,
366
557
  setCursorNativeReplayIdleDisposeMs,
367
558
  resetCursorNativeReplayIdleDisposeMs,
559
+ releaseAllPendingCursorLiveRunsForTests,
368
560
  resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
369
561
  };
@@ -1,6 +1,7 @@
1
- import type { ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
1
+ import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent, TurnStartEvent } from "@earendil-works/pi-coding-agent";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
+ import { isCursorModel } from "./cursor-model.js";
4
5
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge.js";
5
6
 
6
7
  export const CURSOR_ASK_QUESTION_TOOL_NAME = "cursor_ask_question";
@@ -36,6 +37,8 @@ interface CursorQuestionDetails {
36
37
 
37
38
  interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> {
38
39
  on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
40
+ on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
41
+ on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
39
42
  on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
40
43
  }
41
44
 
@@ -81,10 +84,6 @@ const CursorAskQuestionParamsSchema = Type.Object({
81
84
  questions: Type.Optional(Type.Array(QuestionSchema, { description: "Ask multiple questions sequentially" })),
82
85
  });
83
86
 
84
- function isCursorModel(model: ExtensionContext["model"]): boolean {
85
- return model?.provider === "cursor" || model?.api === "cursor-sdk";
86
- }
87
-
88
87
  function normalizeOption(option: RawQuestionOption, index: number): CursorQuestionOption | undefined {
89
88
  if (typeof option === "string") {
90
89
  const trimmed = option.trim();
@@ -246,6 +245,12 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
246
245
  pi.on("session_start", (_event, ctx) => {
247
246
  syncCursorQuestionToolForModel(pi, ctx.model);
248
247
  });
248
+ pi.on("before_agent_start", (_event, ctx) => {
249
+ syncCursorQuestionToolForModel(pi, ctx.model);
250
+ });
251
+ pi.on("turn_start", (_event, ctx) => {
252
+ syncCursorQuestionToolForModel(pi, ctx.model);
253
+ });
249
254
  pi.on("model_select", (event) => {
250
255
  syncCursorQuestionToolForModel(pi, event.model);
251
256
  });