pi-cursor-sdk 0.1.55 → 0.1.57

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 (88) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/README.md +100 -28
  3. package/docs/cursor-live-smoke-checklist.md +7 -7
  4. package/docs/cursor-model-ux-spec.md +48 -41
  5. package/docs/cursor-native-tool-replay.md +4 -4
  6. package/docs/cursor-native-tool-visual-audit.md +1 -1
  7. package/docs/cursor-testing-lessons.md +20 -7
  8. package/docs/cursor-tool-surfaces.md +13 -2
  9. package/docs/platform-smoke-implementation.md +220 -0
  10. package/docs/platform-smoke.md +183 -247
  11. package/package.json +39 -7
  12. package/platform-smoke.config.mjs +5 -1
  13. package/scripts/cloud-runtime-smoke.d.mts +3 -0
  14. package/scripts/cloud-runtime-smoke.mjs +502 -0
  15. package/scripts/debug-provider-events.mjs +7 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -6
  17. package/scripts/lib/cursor-child-process.d.mts +1 -0
  18. package/scripts/lib/cursor-child-process.mjs +137 -7
  19. package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
  20. package/scripts/local-resume-cleanup-smoke.mjs +108 -0
  21. package/scripts/local-resume-smoke.d.mts +1 -0
  22. package/scripts/local-resume-smoke.mjs +642 -0
  23. package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
  24. package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
  25. package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
  26. package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
  27. package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
  28. package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
  29. package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
  30. package/scripts/platform-smoke/artifacts.mjs +293 -65
  31. package/scripts/platform-smoke/card-detect.mjs +16 -4
  32. package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
  33. package/scripts/platform-smoke/doctor.mjs +20 -10
  34. package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
  35. package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
  36. package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
  37. package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
  38. package/scripts/platform-smoke/scenarios.mjs +16 -2
  39. package/scripts/platform-smoke/target-runtime.mjs +206 -0
  40. package/scripts/platform-smoke/targets.mjs +33 -141
  41. package/scripts/platform-smoke/visual-evidence.mjs +6 -7
  42. package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
  43. package/scripts/platform-smoke.mjs +40 -27
  44. package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
  45. package/scripts/steering-rpc-smoke.mjs +12 -2
  46. package/scripts/tmux-live-smoke.sh +3 -5
  47. package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
  48. package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
  49. package/shared/cursor-sensitive-text.mjs +7 -1
  50. package/src/context.ts +5 -2
  51. package/src/cursor-agents-context-registration.ts +7 -0
  52. package/src/cursor-agents-context.ts +3 -1
  53. package/src/cursor-api-key.ts +15 -1
  54. package/src/cursor-bridge-contract.ts +3 -0
  55. package/src/cursor-cloud-lifecycle.ts +733 -0
  56. package/src/cursor-cloud-options.ts +206 -0
  57. package/src/cursor-cloud-reporting.ts +246 -0
  58. package/src/cursor-config.ts +659 -0
  59. package/src/cursor-display-only-trace.ts +14 -0
  60. package/src/cursor-display-text.ts +8 -2
  61. package/src/cursor-durable-fs.ts +49 -0
  62. package/src/cursor-fallback-models.generated.ts +2045 -485
  63. package/src/cursor-live-run-accounting.ts +7 -1
  64. package/src/cursor-live-run-coordinator.ts +1 -0
  65. package/src/cursor-pi-tool-bridge-run.ts +14 -4
  66. package/src/cursor-provider-errors.ts +31 -5
  67. package/src/cursor-provider-live-run-drain.ts +5 -0
  68. package/src/cursor-provider-run-finalizer.ts +37 -28
  69. package/src/cursor-provider-run-outcome.ts +7 -2
  70. package/src/cursor-provider-turn-finalize.ts +77 -7
  71. package/src/cursor-provider-turn-prepare.ts +228 -12
  72. package/src/cursor-provider-turn-runner.ts +42 -13
  73. package/src/cursor-provider-turn-send.ts +59 -16
  74. package/src/cursor-provider-turn-types.ts +44 -10
  75. package/src/cursor-runtime-state.ts +478 -0
  76. package/src/cursor-sdk-event-debug.ts +46 -6
  77. package/src/cursor-sdk-process-error-guard.ts +101 -30
  78. package/src/cursor-session-agent-cleanup.ts +328 -0
  79. package/src/cursor-session-agent-resume.ts +439 -0
  80. package/src/cursor-session-agent.ts +109 -13
  81. package/src/cursor-session-scope.ts +35 -2
  82. package/src/cursor-session-send-policy.ts +1 -1
  83. package/src/cursor-skill-tool.ts +30 -11
  84. package/src/cursor-state.ts +112 -69
  85. package/src/cursor-tool-manifest.ts +1 -1
  86. package/src/cursor-usage-accounting.ts +14 -4
  87. package/src/index.ts +11 -2
  88. package/src/model-discovery.ts +10 -56
@@ -38,7 +38,13 @@ export function takeCursorLiveSdkTurnUsage(state: CursorLiveRunAccountingState):
38
38
  sdkTurnUsage?: CursorSdkTurnUsage;
39
39
  } {
40
40
  const { sdkTurnUsage, ...nextState } = state;
41
- return { state: { ...nextState, sdkTurnEnded: false }, sdkTurnUsage };
41
+ return {
42
+ state: {
43
+ ...nextState,
44
+ sdkTurnEnded: false,
45
+ },
46
+ sdkTurnUsage,
47
+ };
42
48
  }
43
49
 
44
50
  function asToolResultMessage(message: Message): ToolResultMessage | undefined {
@@ -48,6 +48,7 @@ export interface CursorLiveRun {
48
48
  emittedText: string;
49
49
  recordedToolDisplayIds: string[];
50
50
  finalText?: string;
51
+ resumeNotice?: string;
51
52
  done: boolean;
52
53
  cancelled: boolean;
53
54
  disposed: boolean;
@@ -150,6 +150,16 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
150
150
  this.debugRecorder = recorder;
151
151
  }
152
152
 
153
+ private recordBridgeRaw(
154
+ payload: Parameters<NonNullable<CursorPiToolBridgeRunOptions["debugRecorder"]>["recordBridgeRaw"]>[0],
155
+ ): void {
156
+ try {
157
+ this.debugRecorder?.recordBridgeRaw(payload);
158
+ } catch {
159
+ // Debug capture must never block or strand a bridge call.
160
+ }
161
+ }
162
+
153
163
  async resolveToolResults(toolResults: readonly ToolResultMessage[]): Promise<void> {
154
164
  let resolvedCount = 0;
155
165
  for (const toolResult of toolResults) {
@@ -298,11 +308,11 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
298
308
  }
299
309
  this.queuedRequests.push(request);
300
310
  this.emitRequestQueuedDiagnostic(request);
301
- this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
311
+ this.recordBridgeRaw({ kind: "queued", request });
302
312
  return;
303
313
  }
304
314
  this.emitRequestQueuedDiagnostic(request);
305
- this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
315
+ this.recordBridgeRaw({ kind: "queued", request });
306
316
  this.dispatchPendingToolRequest(pending, this.onToolRequest);
307
317
  });
308
318
  }
@@ -331,7 +341,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
331
341
  pending.settled = true;
332
342
  this.removePending(pending);
333
343
  this.emitRequestResolvedDiagnostic(pending.request, result.isError === true);
334
- this.debugRecorder?.recordBridgeRaw({ kind: "resolved", request: pending.request, result });
344
+ this.recordBridgeRaw({ kind: "resolved", request: pending.request, result });
335
345
  pending.resolve(result);
336
346
  }
337
347
 
@@ -340,7 +350,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
340
350
  pending.settled = true;
341
351
  this.removePending(pending);
342
352
  this.emitRequestRejectedDiagnostic(pending.request, kind);
343
- this.debugRecorder?.recordBridgeRaw({
353
+ this.recordBridgeRaw({
344
354
  kind: "rejected",
345
355
  request: pending.request,
346
356
  error: error.message,
@@ -1,4 +1,4 @@
1
- import type { RunResult } from "@cursor/sdk";
1
+ import type { RunError, RunResult } from "@cursor/sdk";
2
2
  import { asRecord } from "./cursor-record-utils.js";
3
3
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
4
 
@@ -15,7 +15,7 @@ const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
15
15
  // Keep this phrase aligned with pi's agent-level retry classifier (`provider.?returned.?error`).
16
16
  const RETRYABLE_CURSOR_RUN_FAILURE_PREFIX = "Provider returned error: Cursor SDK run failed";
17
17
 
18
- export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "requestId" | "status" | "durationMs" | "model" | "result">;
18
+ export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "requestId" | "status" | "durationMs" | "model" | "result" | "error">;
19
19
 
20
20
  function isGenericErrorMessage(message: string): boolean {
21
21
  const normalized = message.trim().toLowerCase();
@@ -208,17 +208,43 @@ function shortRunId(runId: string): string {
208
208
  return `${trimmed.slice(0, 8)}…`;
209
209
  }
210
210
 
211
- export function formatCursorSdkRunFailureDetail(result: CursorSdkRunFailureSource, runResult?: string): string {
211
+ function runErrorCode(error: RunError | undefined): string | undefined {
212
+ return error?.code?.trim() || undefined;
213
+ }
214
+
215
+ function nonGenericRunErrorMessage(error: RunError | undefined): string | undefined {
216
+ const message = error?.message?.trim();
217
+ return message && !isKnownGenericRunFailureText(message) ? message : undefined;
218
+ }
219
+
220
+ function withRunErrorCode(message: string, code: string | undefined): string {
221
+ return code ? `${message} (code: ${code})` : message;
222
+ }
223
+
224
+ export function formatCursorSdkRunFailureDetail(
225
+ result: CursorSdkRunFailureSource,
226
+ runResult?: string,
227
+ runError?: RunError,
228
+ ): string {
229
+ const errorCode = runErrorCode(result.error) ?? runErrorCode(runError);
230
+ const fromWaitError = nonGenericRunErrorMessage(result.error);
231
+ if (fromWaitError) return withRunErrorCode(fromWaitError, errorCode);
232
+
212
233
  const fromWait = result.result?.trim();
213
234
  if (fromWait && !isKnownGenericRunFailureText(fromWait)) {
214
- return fromWait;
235
+ return withRunErrorCode(fromWait, errorCode);
215
236
  }
237
+
238
+ const fromRunError = nonGenericRunErrorMessage(runError);
239
+ if (fromRunError) return withRunErrorCode(fromRunError, errorCode);
240
+
216
241
  const fromRun = runResult?.trim();
217
242
  if (fromRun && !isKnownGenericRunFailureText(fromRun)) {
218
- return fromRun;
243
+ return withRunErrorCode(fromRun, errorCode);
219
244
  }
220
245
 
221
246
  const parts = [RETRYABLE_CURSOR_RUN_FAILURE_PREFIX];
247
+ if (errorCode) parts.push(`code ${errorCode}`);
222
248
  if (result.model?.id) parts.push(`model ${result.model.id}`);
223
249
  parts.push(`run ${shortRunId(result.id)}`);
224
250
  if (result.requestId) parts.push(`request ${shortRunId(result.requestId)}`);
@@ -22,6 +22,7 @@ import { type CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
22
22
  import { resetSessionCursorAgent } from "./cursor-session-agent.js";
23
23
  import { applyCursorUsage } from "./cursor-usage-accounting.js";
24
24
  import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
25
+ import { emitDisplayOnlyTraceBlock } from "./cursor-display-only-trace.js";
25
26
  import { trimCurrentTurnAlreadyEmittedCursorText } from "./cursor-run-final-text.js";
26
27
  import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
27
28
  import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
@@ -379,6 +380,10 @@ export async function drainCursorLiveRunTurn(
379
380
  applyCursorUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens), {
380
381
  turn: cursorLiveRuns.takeSdkTurnUsage(run),
381
382
  });
383
+ if (run.resumeNotice) {
384
+ emitDisplayOnlyTraceBlock(stream, partial, run.resumeNotice);
385
+ run.resumeNotice = undefined;
386
+ }
382
387
  partial.stopReason = "stop";
383
388
  stream.push({ type: "done", reason: "stop", message: partial });
384
389
  await cursorLiveRuns.release(run);
@@ -1,5 +1,5 @@
1
1
  import type { AssistantMessage } from "@earendil-works/pi-ai/compat";
2
- import { abandonSessionCursorAgent, cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
2
+ import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
3
3
  import {
4
4
  classifyCursorRunEmission,
5
5
  getCursorRunAbortMessage,
@@ -23,27 +23,32 @@ import type {
23
23
  CursorProviderTurnRunnerParams,
24
24
  CursorProviderTurnSend,
25
25
  CursorProviderTurnSendResult,
26
+ LiveCursorProviderTurnRuntime,
27
+ LocalCursorProviderTurnPrepareResult,
26
28
  } from "./cursor-provider-turn-types.js";
27
29
  import { applyCursorUsage } from "./cursor-usage-accounting.js";
28
30
  import { hasUsableText } from "./cursor-record-utils.js";
31
+ import { emitDisplayOnlyTraceBlock } from "./cursor-display-only-trace.js";
29
32
  export type CursorTurnTerminalEvent =
30
33
  | {
31
34
  kind: "direct";
32
35
  prepared: CursorProviderTurnPrepareResult;
33
36
  outcome: CursorRunOutcome;
37
+ displayOnlyTraceBlock?: string;
34
38
  }
35
39
  | { kind: "error"; prepared: CursorProviderTurnPrepareResult | undefined; error: unknown };
36
40
 
37
41
  function applyLiveRunOutcome(
38
42
  outcome: CursorRunOutcome,
39
- prepared: CursorProviderTurnPrepareResult,
43
+ prepared: LocalCursorProviderTurnPrepareResult & { runtime: LiveCursorProviderTurnRuntime },
40
44
  context: CursorProviderTurnRunnerParams["context"],
41
45
  ): void {
42
- if (prepared.runtime.kind !== "live" || prepared.runtime.liveRun.disposed) return;
46
+ if (prepared.runtime.liveRun.disposed) return;
43
47
  const { liveRun } = prepared.runtime;
44
48
  switch (classifyCursorRunEmission(outcome)) {
45
49
  case "finished":
46
- prepared.sessionAgentLease.commitSend(context, prepared.meta.bootstrap);
50
+ prepared.lifecycle.commitSend(context, prepared.meta.bootstrap);
51
+ if (prepared.meta.resumeNotice) liveRun.resumeNotice = prepared.meta.resumeNotice;
47
52
  cursorLiveRuns.markFinished(liveRun, outcome.kind === "finished" ? outcome.finalText : "");
48
53
  break;
49
54
  case "cancelled":
@@ -69,7 +74,7 @@ export interface CursorRunFinalizerParams {
69
74
 
70
75
  export interface StartCursorLiveRunCompletionParams {
71
76
  send: CursorProviderTurnSend;
72
- prepared: CursorProviderTurnPrepareResult;
77
+ prepared: LocalCursorProviderTurnPrepareResult & { runtime: LiveCursorProviderTurnRuntime };
73
78
  modelId: string;
74
79
  discardIncompleteTools: (outcome: IncompleteCursorToolRunOutcomeInput) => void;
75
80
  }
@@ -84,7 +89,6 @@ export class CursorRunFinalizer {
84
89
  const sdkEventDebug = this.params.sdkEventDebug();
85
90
  const { send, prepared, modelId, discardIncompleteTools } = startParams;
86
91
  const { run, cursorAgentMessageOffset } = send;
87
- if (prepared.runtime.kind !== "live") throw new Error("startLiveRunCompletion requires a live run");
88
92
  const { liveRun } = prepared.runtime;
89
93
  const waitCompletion = awaitFinalizeCursorRunOutcome({
90
94
  run,
@@ -93,36 +97,37 @@ export class CursorRunFinalizer {
93
97
  modelId,
94
98
  signal: runnerParams.options?.signal,
95
99
  runResultFallback: run.result,
100
+ runErrorFallback: run.error,
96
101
  resolvedApiKey: this.params.resolvedApiKey(),
97
102
  optionsApiKey: runnerParams.options?.apiKey,
98
103
  sdkEventDebug,
99
104
  cacheContextWindow: true,
100
105
  contextWindowAgentId: liveRun.agent.agentId,
101
106
  })
102
- .then(async (outcome) => {
103
- applyLiveRunOutcome(outcome, prepared, runnerParams.context);
107
+ .then(async (finalized) => {
108
+ applyLiveRunOutcome(finalized.outcome, prepared, runnerParams.context);
104
109
  })
105
- .catch(async (error: unknown) => {
106
- sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) });
107
- sdkEventDebug?.recordError("run_wait", error);
108
- discardIncompleteTools({ status: "error" });
109
- await sdkEventDebug?.captureRunArtifacts(run);
110
- if (liveRun.disposed) return;
111
- cursorLiveRuns.markError(
112
- liveRun,
113
- sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? runnerParams.options?.apiKey),
114
- );
110
+ .catch((error: unknown) => {
111
+ this.safeCleanup(() => discardIncompleteTools({ status: "error" }));
112
+ if (!liveRun.disposed) {
113
+ cursorLiveRuns.markError(
114
+ liveRun,
115
+ sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? runnerParams.options?.apiKey),
116
+ );
117
+ }
118
+ this.safeCleanup(() => sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) }));
119
+ this.safeCleanup(() => sdkEventDebug?.recordError("run_wait", error));
115
120
  });
116
- // Mark the pooled agent busy as soon as the SDK run exists so auto-compaction summarization
121
+ // Mark the pooled local agent busy as soon as the SDK run exists so auto-compaction summarization
117
122
  // (and other concurrent acquires) wait for run.wait() instead of hitting AgentBusyError.
118
- prepared.sessionAgentLease.trackRunCompletion(waitCompletion);
123
+ prepared.lifecycle.trackRunCompletion(waitCompletion);
119
124
  return { waitCompletion, prepared };
120
125
  }
121
126
 
122
127
  async applyTerminalEvent(event: CursorTurnTerminalEvent): Promise<void> {
123
128
  if (this.terminalApplied) return;
124
129
  if (event.kind === "direct") {
125
- await this.applyDirectOutcome(event.prepared, event.outcome);
130
+ await this.applyDirectOutcome(event.prepared, event.outcome, event.displayOnlyTraceBlock);
126
131
  this.terminalApplied = true;
127
132
  return;
128
133
  }
@@ -150,6 +155,7 @@ export class CursorRunFinalizer {
150
155
  .catch(() => {});
151
156
  return;
152
157
  }
158
+ await prepared?.lifecycle.dispose().catch(() => {});
153
159
  await this.finalizeSdkEventDebugBestEffort();
154
160
  this.safeCleanup(() => this.params.sdkProcessErrorGuard.dispose());
155
161
  }
@@ -157,45 +163,48 @@ export class CursorRunFinalizer {
157
163
  private async applyDirectOutcome(
158
164
  prepared: CursorProviderTurnPrepareResult,
159
165
  outcome: CursorRunOutcome,
166
+ displayOnlyTraceBlock: string | undefined,
160
167
  ): Promise<void> {
161
168
  const { stream, partial, model, context } = this.params.runnerParams;
162
169
  prepared.runtime.turnCoordinator.closeTraceBlock();
163
170
  switch (classifyCursorRunEmission(outcome)) {
164
171
  case "cancelled":
165
- await abandonSessionCursorAgent(prepared.sessionAgentScopeKey);
172
+ await prepared.lifecycle.abandon();
166
173
  this.pushTerminalError(partial, "aborted", getCursorRunAbortMessage(outcome));
167
174
  break;
168
175
  case "failed":
169
- await abandonSessionCursorAgent(prepared.sessionAgentScopeKey);
176
+ await prepared.lifecycle.abandon();
170
177
  this.pushTerminalError(partial, "error", outcome.kind === "error" ? outcome.errorMessage : "Cursor SDK run failed.");
171
178
  break;
172
179
  case "finished":
173
- prepared.sessionAgentLease.commitSend(context, prepared.meta.bootstrap);
180
+ prepared.lifecycle.commitSend(context, prepared.meta.bootstrap);
174
181
  prepared.runtime.turnCoordinator.flushText(
175
182
  outcome.kind === "finished" && hasUsableText(outcome.finalText) ? [outcome.finalText] : [],
176
183
  );
177
184
  applyCursorUsage(partial, model, context, prepared.meta.promptInputTokens, {
178
185
  turn: prepared.runtime.turnCoordinator.lastSdkTurnUsage,
179
186
  });
187
+ if (prepared.meta.resumeNotice) emitDisplayOnlyTraceBlock(stream, partial, prepared.meta.resumeNotice);
188
+ if (displayOnlyTraceBlock) emitDisplayOnlyTraceBlock(stream, partial, displayOnlyTraceBlock);
180
189
  stream.push({ type: "done", reason: "stop", message: partial });
181
190
  break;
182
191
  }
183
192
  }
184
193
 
185
194
  private async applyErrorOutcome(prepared: CursorProviderTurnPrepareResult | undefined, error: unknown): Promise<void> {
186
- this.params.sdkEventDebug()?.recordError("provider_stream", error);
187
- prepared?.runtime.turnCoordinator.discardIncompleteStartedToolCalls(
195
+ this.safeCleanup(() => prepared?.runtime.turnCoordinator.discardIncompleteStartedToolCalls(
188
196
  buildIncompleteCursorToolRunOutcome({
189
197
  status: error instanceof CursorLiveRunAbortError ? "cancelled" : "error",
190
198
  signalAborted: error instanceof CursorLiveRunAbortError,
191
199
  }),
192
- );
200
+ ));
193
201
  const activeLiveRun = prepared?.runtime.liveRun;
194
202
  if (activeLiveRun && !activeLiveRun.disposed) {
195
203
  await cursorLiveRuns.release(activeLiveRun);
196
204
  } else {
197
- await abandonSessionCursorAgent(prepared?.sessionAgentScopeKey);
205
+ await prepared?.lifecycle.abandon();
198
206
  }
207
+ this.safeCleanup(() => this.params.sdkEventDebug()?.recordError("provider_stream", error));
199
208
  if (error instanceof CursorLiveRunAbortError) {
200
209
  this.params.sdkProcessErrorGuard.suppressAbortErrors();
201
210
  this.pushTerminalError(this.params.runnerParams.partial, "aborted", this.abortMessage());
@@ -1,4 +1,4 @@
1
- import type { RunResult } from "@cursor/sdk";
1
+ import type { RunError, RunResult } from "@cursor/sdk";
2
2
  import { selectCursorFinalText } from "./cursor-run-final-text.js";
3
3
  import {
4
4
  formatCursorSdkAbortMessage,
@@ -42,6 +42,7 @@ export interface ResolveCursorRunOutcomeParams {
42
42
  planTextCandidate?: string;
43
43
  selectFinalTextOptions?: { allowPartialPrefix?: boolean };
44
44
  runResultFallback?: string;
45
+ runErrorFallback?: RunError;
45
46
  resolvedApiKey?: string;
46
47
  optionsApiKey?: string;
47
48
  }
@@ -91,7 +92,11 @@ export function resolveCursorRunOutcome(params: ResolveCursorRunOutcomeParams):
91
92
  }
92
93
 
93
94
  if (waitResult.status === "error") {
94
- const failureDetail = formatCursorSdkRunFailureDetail(waitResult, params.runResultFallback);
95
+ const failureDetail = formatCursorSdkRunFailureDetail(
96
+ waitResult,
97
+ params.runResultFallback,
98
+ params.runErrorFallback,
99
+ );
95
100
  return {
96
101
  kind: "error",
97
102
  waitResult,
@@ -1,6 +1,13 @@
1
- import type { SDKAgent } from "@cursor/sdk";
1
+ import type { RunError, SDKAgent } from "@cursor/sdk";
2
2
  import { loadCursorTranscriptWebToolCallsAfterOffset } from "./cursor-agent-message-web-tools.js";
3
+ import {
4
+ collectCursorCloudRunReport,
5
+ formatCursorCloudRunReport,
6
+ type CursorCloudRunReport,
7
+ } from "./cursor-cloud-reporting.js";
8
+ import { recordCursorCloudLifecycleRun } from "./cursor-cloud-lifecycle.js";
3
9
  import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
10
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
11
  import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
5
12
  import type { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
6
13
  import {
@@ -30,6 +37,7 @@ export interface BuildCursorRunOutcomeParams {
30
37
  prepared: CursorProviderTurnPrepareResult;
31
38
  signal?: AbortSignal;
32
39
  runResultFallback?: string;
40
+ runErrorFallback?: RunError;
33
41
  resolvedApiKey?: string;
34
42
  optionsApiKey?: string;
35
43
  }
@@ -46,6 +54,7 @@ export function buildCursorRunOutcomeFromWait(params: BuildCursorRunOutcomeParam
46
54
  planTextCandidate: turnCoordinator.planTextCandidate,
47
55
  selectFinalTextOptions: liveRun ? undefined : { allowPartialPrefix: true },
48
56
  runResultFallback: params.runResultFallback,
57
+ runErrorFallback: params.runErrorFallback,
49
58
  resolvedApiKey: params.resolvedApiKey,
50
59
  optionsApiKey: params.optionsApiKey,
51
60
  });
@@ -76,6 +85,22 @@ async function replayCursorTranscriptWebToolCalls(
76
85
  }
77
86
  }
78
87
 
88
+ function scrubCursorCloudReportingError(error: unknown, apiKey: string | undefined): Error {
89
+ return new Error(scrubSensitiveText(error instanceof Error ? error.message : String(error), apiKey));
90
+ }
91
+
92
+ function recordCursorCloudReportingError(
93
+ sdkEventDebug: CursorSdkEventDebugSink | undefined,
94
+ error: unknown,
95
+ apiKey: string | undefined,
96
+ ): void {
97
+ try {
98
+ sdkEventDebug?.recordError("cloud_run_report", scrubCursorCloudReportingError(error, apiKey));
99
+ } catch {
100
+ // Debug reporting must never affect provider execution.
101
+ }
102
+ }
103
+
79
104
  export interface AwaitFinalizeCursorRunOutcomeParams {
80
105
  run: Awaited<ReturnType<SDKAgent["send"]>>;
81
106
  prepared: CursorProviderTurnPrepareResult;
@@ -83,6 +108,7 @@ export interface AwaitFinalizeCursorRunOutcomeParams {
83
108
  modelId: string;
84
109
  signal?: AbortSignal;
85
110
  runResultFallback?: string;
111
+ runErrorFallback?: RunError;
86
112
  resolvedApiKey?: string;
87
113
  optionsApiKey?: string;
88
114
  sdkEventDebug?: CursorSdkEventDebugSink;
@@ -92,19 +118,59 @@ export interface AwaitFinalizeCursorRunOutcomeParams {
92
118
  contextWindowAgentId?: string;
93
119
  }
94
120
 
121
+ export interface FinalizedCursorRunOutcome {
122
+ outcome: CursorRunOutcome;
123
+ displayOnlyTraceBlock?: string;
124
+ }
125
+
95
126
  /** Single wait/finalize path for SDK runs: wait, debug capture, transcript replay, incomplete tools, artifacts, context cache. */
96
- export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorRunOutcomeParams): Promise<CursorRunOutcome> {
127
+ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorRunOutcomeParams): Promise<FinalizedCursorRunOutcome> {
128
+ const apiKey = params.resolvedApiKey ?? params.optionsApiKey;
97
129
  const waitResult = params.waitResult ?? (await params.run.wait());
98
- params.sdkEventDebug?.recordWaitResult(waitResult);
99
130
  const outcome = buildCursorRunOutcomeFromWait({
100
131
  waitResult,
101
132
  prepared: params.prepared,
102
133
  signal: params.signal,
103
134
  runResultFallback: params.runResultFallback,
135
+ runErrorFallback: params.runErrorFallback,
104
136
  resolvedApiKey: params.resolvedApiKey,
105
137
  optionsApiKey: params.optionsApiKey,
106
138
  });
107
- if (isCursorRunFinishedSuccessfully(outcome)) {
139
+ let displayOnlyTraceBlock: string | undefined;
140
+ if (params.prepared.runtimeTarget === "cloud" && isCursorRunFinishedSuccessfully(outcome)) {
141
+ let report: CursorCloudRunReport = { agentId: params.run.agentId, runId: params.run.id, branches: [] };
142
+ try {
143
+ report = await collectCursorCloudRunReport({
144
+ agent: params.prepared.agent,
145
+ run: params.run,
146
+ waitResult,
147
+ apiKey,
148
+ });
149
+ } catch (error) {
150
+ recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
151
+ }
152
+ try {
153
+ recordCursorCloudLifecycleRun(report, { apiKey });
154
+ } catch (error) {
155
+ recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
156
+ }
157
+ try {
158
+ params.sdkEventDebug?.recordProviderEvent("cloud_run_report", report);
159
+ } catch (error) {
160
+ recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
161
+ }
162
+ try {
163
+ displayOnlyTraceBlock = formatCursorCloudRunReport(report, { apiKey });
164
+ } catch (error) {
165
+ recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
166
+ }
167
+ }
168
+ try {
169
+ params.sdkEventDebug?.recordWaitResult(waitResult);
170
+ } catch {
171
+ // Debug reporting must never affect provider execution.
172
+ }
173
+ if (params.prepared.runtimeTarget === "local" && isCursorRunFinishedSuccessfully(outcome)) {
108
174
  await replayCursorTranscriptWebToolCalls(
109
175
  params.run.agentId,
110
176
  params.prepared.cwd,
@@ -114,9 +180,13 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
114
180
  );
115
181
  }
116
182
  params.prepared.runtime.turnCoordinator.discardIncompleteStartedToolCalls(outcome.incompleteTools);
117
- await params.sdkEventDebug?.captureRunArtifacts(params.run);
118
- if (params.cacheContextWindow !== false) {
183
+ try {
184
+ await params.sdkEventDebug?.captureRunArtifacts(params.run);
185
+ } catch {
186
+ // Debug artifact failures must never affect provider execution.
187
+ }
188
+ if (params.prepared.runtimeTarget === "local" && params.cacheContextWindow !== false) {
119
189
  await cacheSdkContextWindow(params.contextWindowAgentId ?? params.run.agentId, params.modelId, params.prepared.cwd);
120
190
  }
121
- return outcome;
191
+ return { outcome, displayOnlyTraceBlock };
122
192
  }