pi-cursor-sdk 0.1.18 → 0.1.20

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