pi-cursor-sdk 0.1.19 → 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.
@@ -1,34 +1,58 @@
1
- import {
2
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
3
- getCursorReplayDisplayLabel,
4
- type CursorReplayLegacyToolName,
5
- } from "./cursor-tool-names.js";
1
+ import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME } from "./cursor-tool-names.js";
6
2
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
7
3
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
8
4
  import {
9
5
  DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
10
6
  type DiscardedIncompleteStartedToolCallReason,
11
7
  } from "./cursor-sdk-event-debug.js";
12
- import { getToolArgs, getToolName, normalizeToolName, truncateArg, type CursorPiToolDisplay } from "./cursor-transcript-utils.js";
13
- import { resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
8
+ import { truncateArg, type CursorPiToolDisplay } from "./cursor-transcript-utils.js";
9
+ import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
14
10
 
15
11
  export type IncompleteCursorToolDiscardReason = DiscardedIncompleteStartedToolCallReason;
16
12
 
17
- const INCOMPLETE_TITLE_KEYS: Partial<Record<string, CursorReplayLegacyToolName>> = {
18
- task: "cursor_task",
19
- mcp: "cursor_mcp",
20
- generateimage: "cursor_generate_image",
21
- recordscreen: "cursor_record_screen",
22
- semsearch: "cursor_sem_search",
23
- websearch: "cursor_web_search",
24
- webfetch: "cursor_web_fetch",
25
- createplan: "cursor_create_plan",
26
- updatetodos: "cursor_update_todos",
27
- readlints: "cursor_read_lints",
28
- delete: "cursor_delete",
29
- edit: "cursor_edit",
30
- write: "cursor_write",
31
- };
13
+ export interface IncompleteCursorToolRunOutcome {
14
+ reason: IncompleteCursorToolDiscardReason;
15
+ assistantTextProduced: boolean;
16
+ }
17
+
18
+ export interface IncompleteCursorToolRunOutcomeInput {
19
+ reason?: IncompleteCursorToolDiscardReason;
20
+ status?: string;
21
+ signalAborted?: boolean;
22
+ assistantTextProduced?: boolean;
23
+ }
24
+
25
+ export type IncompleteCursorToolVisibilityDecision = "emit" | "suppress" | "debugOnly";
26
+
27
+ export function buildIncompleteCursorToolRunOutcome(
28
+ outcome: IncompleteCursorToolRunOutcomeInput = {},
29
+ ): IncompleteCursorToolRunOutcome {
30
+ return {
31
+ reason:
32
+ outcome.reason ??
33
+ (outcome.status === "cancelled" || outcome.signalAborted
34
+ ? "abort"
35
+ : outcome.status === "error"
36
+ ? "sdk-failure"
37
+ : DISCARDED_INCOMPLETE_TOOL_CALL_REASON),
38
+ assistantTextProduced: outcome.assistantTextProduced ?? false,
39
+ };
40
+ }
41
+
42
+ export function resolveIncompleteCursorToolVisibility(
43
+ toolCall: unknown,
44
+ outcome: IncompleteCursorToolRunOutcome,
45
+ ): IncompleteCursorToolVisibilityDecision {
46
+ const visibility = classifyCursorToolVisibility(toolCall);
47
+ if (
48
+ outcome.reason === DISCARDED_INCOMPLETE_TOOL_CALL_REASON &&
49
+ outcome.assistantTextProduced &&
50
+ visibility.fastLocalDiscovery
51
+ ) {
52
+ return "debugOnly";
53
+ }
54
+ return "emit";
55
+ }
32
56
 
33
57
  function buildGenericIncompleteActivityTitle(displayName: string): string {
34
58
  if (!displayName || displayName === "unknown") return "Cursor tool";
@@ -49,25 +73,8 @@ export function formatIncompleteCursorToolReasonText(reason: IncompleteCursorToo
49
73
  }
50
74
 
51
75
  export function getIncompleteCursorToolActivityTitle(toolCall: unknown): string {
52
- const args = getToolArgs(toolCall);
53
- const name = resolveTranscriptToolName(getToolName(toolCall), args);
54
- const normalized = normalizeToolName(name).toLowerCase();
55
- const labelKey = INCOMPLETE_TITLE_KEYS[normalized];
56
- if (labelKey) return getCursorReplayDisplayLabel(labelKey);
57
- switch (normalized) {
58
- case "read":
59
- return "Cursor read";
60
- case "shell":
61
- return "Cursor shell";
62
- case "grep":
63
- return "Cursor grep";
64
- case "glob":
65
- return "Cursor find";
66
- case "ls":
67
- return "Cursor ls";
68
- default:
69
- return buildGenericIncompleteActivityTitle(name);
70
- }
76
+ const visibility = classifyCursorToolVisibility(toolCall);
77
+ return visibility.incompleteTitle ?? buildGenericIncompleteActivityTitle(visibility.displayName);
71
78
  }
72
79
 
73
80
  export function buildIncompleteCursorToolDisplay(
@@ -75,8 +82,7 @@ export function buildIncompleteCursorToolDisplay(
75
82
  reason: IncompleteCursorToolDiscardReason,
76
83
  options: { apiKey?: string } = {},
77
84
  ): CursorPiToolDisplay {
78
- const args = getToolArgs(toolCall);
79
- const transcriptName = resolveTranscriptToolName(getToolName(toolCall), args);
85
+ const visibility = classifyCursorToolVisibility(toolCall);
80
86
  const activityTitle = getIncompleteCursorToolActivityTitle(toolCall);
81
87
  const headline = `${activityTitle} did not complete`;
82
88
  const reasonText = scrubSensitiveText(formatIncompleteCursorToolReasonText(reason), options.apiKey);
@@ -84,7 +90,7 @@ export function buildIncompleteCursorToolDisplay(
84
90
  return {
85
91
  toolName: CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
86
92
  args: {
87
- cursorToolName: normalizeToolName(transcriptName),
93
+ cursorToolName: visibility.normalizedName,
88
94
  activityTitle,
89
95
  activitySummary: reasonText,
90
96
  incomplete: true,
@@ -92,7 +98,7 @@ export function buildIncompleteCursorToolDisplay(
92
98
  result: {
93
99
  content: [{ type: "text", text: contentText }],
94
100
  details: {
95
- cursorToolName: normalizeToolName(transcriptName),
101
+ cursorToolName: visibility.normalizedName,
96
102
  title: headline,
97
103
  summary: reasonText,
98
104
  },
@@ -1,17 +1,23 @@
1
1
  const CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS = 60_000;
2
2
  const DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS = 3_600_000;
3
+ const DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS = 10_000;
4
+ const MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS = 1_000;
3
5
  const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
4
6
  const CURSOR_MCP_TOOL_TIMEOUT_MS_ENV = "PI_CURSOR_MCP_TOOL_TIMEOUT_MS";
5
7
  const CURSOR_MCP_TOOL_TIMEOUT_SECONDS_ENV = "PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS";
8
+ const CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV = "PI_CURSOR_MCP_CONNECT_TIMEOUT_MS";
9
+ const CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV = "PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS";
6
10
 
7
11
  interface CursorMcpToolTimeoutOverrideOptions {
8
12
  timeoutMs?: number;
13
+ connectTimeoutMs?: number;
9
14
  env?: Record<string, string | undefined>;
10
15
  }
11
16
 
12
17
  interface CursorMcpToolTimeoutOverrideState {
13
18
  installed: boolean;
14
19
  timeoutMs: number;
20
+ connectTimeoutMs: number;
15
21
  sdkDefaultTimeoutMs: number;
16
22
  }
17
23
 
@@ -20,7 +26,8 @@ type SetTimeoutHandler = Parameters<GlobalSetTimeout>[0];
20
26
  type SetTimeoutDelay = Parameters<GlobalSetTimeout>[1];
21
27
 
22
28
  let originalSetTimeout: GlobalSetTimeout | undefined;
23
- let installedTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
29
+ let installedToolTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
30
+ let installedConnectTimeoutMs = DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
24
31
 
25
32
  function parsePositiveNumber(value: string | undefined): number | undefined {
26
33
  const trimmed = value?.trim();
@@ -46,15 +53,47 @@ export function resolveCursorMcpToolTimeoutMs(
46
53
  return DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
47
54
  }
48
55
 
49
- export function isCursorSdkMcpToolTimeoutStack(stack: string | undefined): boolean {
56
+ function normalizeConnectTimeoutMs(timeoutMs: number): number {
57
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
58
+ return Math.min(
59
+ Math.max(Math.trunc(timeoutMs), MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS),
60
+ CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
61
+ );
62
+ }
63
+
64
+ export function resolveCursorMcpConnectTimeoutMs(
65
+ env: Record<string, string | undefined> = process.env,
66
+ ): number {
67
+ const explicitMs = parsePositiveNumber(env[CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV]);
68
+ if (explicitMs !== undefined) return normalizeConnectTimeoutMs(explicitMs);
69
+
70
+ const explicitSeconds = parsePositiveNumber(env[CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV]);
71
+ if (explicitSeconds !== undefined) return normalizeConnectTimeoutMs(explicitSeconds * 1000);
72
+
73
+ return DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
74
+ }
75
+
76
+ function isCursorSdkMcpProtocolTimeoutStack(stack: string | undefined): boolean {
50
77
  if (!stack) return false;
51
78
  return (
52
79
  /(?:node_modules[/\\]@cursor[/\\]sdk|node_modules\/\@cursor\/sdk|@cursor\/sdk\/dist)/.test(stack) &&
53
- /\b_setupTimeout\b|\bProtocol\._setupTimeout\b/.test(stack) &&
80
+ /\b_setupTimeout\b|\bProtocol\._setupTimeout\b/.test(stack)
81
+ );
82
+ }
83
+
84
+ export function isCursorSdkMcpToolTimeoutStack(stack: string | undefined): boolean {
85
+ if (!stack) return false;
86
+ return (
87
+ isCursorSdkMcpProtocolTimeoutStack(stack) &&
54
88
  /\bcallTool\b|\bClient\.callTool\b|\bMcpSdkClient\.callTool\b/.test(stack)
55
89
  );
56
90
  }
57
91
 
92
+ export function isCursorSdkMcpConnectTimeoutStack(stack: string | undefined): boolean {
93
+ if (!stack || !isCursorSdkMcpProtocolTimeoutStack(stack)) return false;
94
+ return /\bClient\.(?:connect|listTools)\b|\bMcpSdkClient\.getTools\b/.test(stack);
95
+ }
96
+
58
97
  function isCursorSdkDefaultMcpTimeout(delay: SetTimeoutDelay): boolean {
59
98
  return typeof delay === "number" && delay === CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS;
60
99
  }
@@ -67,10 +106,15 @@ function patchedSetTimeout(
67
106
  const delegate = originalSetTimeout;
68
107
  if (!delegate) throw new Error("Cursor MCP timeout override installed without original setTimeout");
69
108
 
70
- const nextDelay =
71
- isCursorSdkDefaultMcpTimeout(delay) && isCursorSdkMcpToolTimeoutStack(new Error().stack)
72
- ? installedTimeoutMs
73
- : delay;
109
+ let nextDelay = delay;
110
+ if (isCursorSdkDefaultMcpTimeout(delay)) {
111
+ const stack = new Error().stack;
112
+ if (isCursorSdkMcpToolTimeoutStack(stack)) {
113
+ nextDelay = installedToolTimeoutMs;
114
+ } else if (isCursorSdkMcpConnectTimeoutStack(stack)) {
115
+ nextDelay = installedConnectTimeoutMs;
116
+ }
117
+ }
74
118
 
75
119
  return Reflect.apply(delegate, globalThis, [handler, nextDelay, ...args]) as ReturnType<GlobalSetTimeout>;
76
120
  }
@@ -78,9 +122,12 @@ function patchedSetTimeout(
78
122
  export function installCursorMcpToolTimeoutOverride(
79
123
  options: CursorMcpToolTimeoutOverrideOptions = {},
80
124
  ): CursorMcpToolTimeoutOverrideState {
81
- installedTimeoutMs = normalizeOverrideTimeoutMs(
125
+ installedToolTimeoutMs = normalizeOverrideTimeoutMs(
82
126
  options.timeoutMs ?? resolveCursorMcpToolTimeoutMs(options.env),
83
127
  );
128
+ installedConnectTimeoutMs = normalizeConnectTimeoutMs(
129
+ options.connectTimeoutMs ?? resolveCursorMcpConnectTimeoutMs(options.env),
130
+ );
84
131
 
85
132
  if (!originalSetTimeout) {
86
133
  originalSetTimeout = globalThis.setTimeout;
@@ -89,23 +136,31 @@ export function installCursorMcpToolTimeoutOverride(
89
136
 
90
137
  return {
91
138
  installed: true,
92
- timeoutMs: installedTimeoutMs,
139
+ timeoutMs: installedToolTimeoutMs,
140
+ connectTimeoutMs: installedConnectTimeoutMs,
93
141
  sdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
94
142
  };
95
143
  }
96
144
 
97
- export function restoreCursorMcpToolTimeoutOverrideForTests(): void {
145
+ export function restoreCursorMcpToolTimeoutOverride(): void {
98
146
  if (originalSetTimeout) {
99
147
  globalThis.setTimeout = originalSetTimeout;
100
148
  originalSetTimeout = undefined;
101
149
  }
102
- installedTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
150
+ installedToolTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
151
+ installedConnectTimeoutMs = DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
103
152
  }
104
153
 
154
+ export const restoreCursorMcpToolTimeoutOverrideForTests = restoreCursorMcpToolTimeoutOverride;
155
+
105
156
  export const cursorMcpToolTimeoutOverrideDefaults = {
106
157
  cursorSdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
107
158
  defaultOverrideTimeoutMs: DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS,
159
+ defaultConnectTimeoutMs: DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS,
160
+ minConnectTimeoutMs: MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS,
108
161
  maxNodeTimerDelayMs: MAX_NODE_TIMER_DELAY_MS,
109
162
  timeoutMsEnv: CURSOR_MCP_TOOL_TIMEOUT_MS_ENV,
110
163
  timeoutSecondsEnv: CURSOR_MCP_TOOL_TIMEOUT_SECONDS_ENV,
164
+ connectTimeoutMsEnv: CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV,
165
+ connectTimeoutSecondsEnv: CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV,
111
166
  } as const;
@@ -35,6 +35,7 @@ export interface CursorReplayToolDetails {
35
35
  diff?: string;
36
36
  firstChangedLine?: number;
37
37
  expandedText?: string;
38
+ collapseDetailsByDefault?: boolean;
38
39
  }
39
40
 
40
41
  export function asCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
@@ -406,7 +407,7 @@ function renderExpandableCursorReplayResult(
406
407
  const summary = details?.summary ?? text.split("\n").find((line) => line.trim()) ?? "completed";
407
408
  let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg(isError ? "error" : "success", summary)}`;
408
409
  const expandedText = details?.expandedText ?? (text.includes("\n") ? text : undefined);
409
- if (expandedText) {
410
+ if (expandedText && (options.expanded || !details?.collapseDetailsByDefault)) {
410
411
  const preview = options.expanded ? formatMutedBlock(expandedText, theme) : formatCursorReplayPreview(expandedText, theme);
411
412
  if (preview) rendered += `\n${preview}`;
412
413
  }
@@ -16,15 +16,19 @@ import {
16
16
  } from "./cursor-sdk-event-debug.js";
17
17
  import {
18
18
  buildIncompleteCursorToolDisplay,
19
+ buildIncompleteCursorToolRunOutcome,
19
20
  formatIncompleteCursorToolTrace,
21
+ resolveIncompleteCursorToolVisibility,
22
+ type IncompleteCursorToolRunOutcome,
20
23
  type IncompleteCursorToolDiscardReason,
21
24
  } from "./cursor-incomplete-tool-visibility.js";
22
- import { getToolName, normalizeToolName } from "./cursor-transcript-utils.js";
25
+ import { getToolName } from "./cursor-transcript-utils.js";
23
26
  import {
24
27
  CURSOR_TOOL_LIFECYCLE_DEFER_MS,
25
28
  formatCursorToolLifecycleProgressText,
26
29
  isCursorToolLifecycleEligible,
27
30
  } from "./cursor-tool-lifecycle.js";
31
+ import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
28
32
 
29
33
  function formatCursorToolName(toolCall: unknown): string {
30
34
  return truncateCursorDisplayLine(getToolName(toolCall), 80) || "unknown";
@@ -40,9 +44,12 @@ interface CursorShellOutputDeltas {
40
44
  stderr: string[];
41
45
  }
42
46
 
47
+ function getNormalizedCursorToolName(toolCall: unknown): string {
48
+ return classifyCursorToolVisibility(toolCall).normalizedName;
49
+ }
50
+
43
51
  function isCursorShellToolCall(toolCall: unknown): boolean {
44
- const normalizedName = getToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase();
45
- return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
52
+ return classifyCursorToolVisibility(toolCall).normalizedKey === "shell";
46
53
  }
47
54
 
48
55
  function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
@@ -162,16 +169,30 @@ export class CursorSdkTurnCoordinator {
162
169
  }
163
170
 
164
171
  discardIncompleteStartedToolCalls(
165
- reason: IncompleteCursorToolDiscardReason = DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
172
+ outcome: IncompleteCursorToolRunOutcome = buildIncompleteCursorToolRunOutcome(),
166
173
  ): void {
167
174
  for (const [callId, toolCall] of this.startedToolCalls) {
168
175
  if (typeof callId !== "string") continue;
176
+ const toolName = getNormalizedCursorToolName(toolCall);
169
177
  recordDiscardedIncompleteStartedToolCall(this.debugRecorder, process.env, {
170
- toolName: normalizeToolName(getToolName(toolCall)),
178
+ toolName,
171
179
  callId,
172
- reason,
180
+ reason: outcome.reason,
173
181
  });
174
- this.emitIncompleteStartedToolCall(toolCall, reason);
182
+ const visibilityDecision = resolveIncompleteCursorToolVisibility(toolCall, outcome);
183
+ if (visibilityDecision !== "emit") {
184
+ this.recordDisplayDecision({
185
+ action: "skip-incomplete-fast-local",
186
+ toolName,
187
+ source: "started",
188
+ reason:
189
+ visibilityDecision === "debugOnly" && outcome.assistantTextProduced
190
+ ? "successful-run-text-produced"
191
+ : visibilityDecision,
192
+ });
193
+ continue;
194
+ }
195
+ this.emitIncompleteStartedToolCall(toolCall, outcome.reason);
175
196
  }
176
197
  this.startedToolCalls.clear();
177
198
  this.bridgeStartedToolCallIds.clear();
@@ -549,7 +570,7 @@ export class CursorSdkTurnCoordinator {
549
570
  this.emittedLifecycleCallIds.add(callId);
550
571
  this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle", {
551
572
  callId,
552
- toolName: normalizeToolName(getToolName(toolCall)),
573
+ toolName: getNormalizedCursorToolName(toolCall),
553
574
  progressText,
554
575
  liveRun: this.liveRun !== undefined,
555
576
  });
@@ -14,7 +14,6 @@ import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-
14
14
  import {
15
15
  acquireSessionCursorAgent,
16
16
  buildCursorSessionSendPrompt,
17
- commitSessionAgentSend,
18
17
  disposeAllSessionCursorAgents,
19
18
  planCursorSessionSend,
20
19
  resetSessionCursorAgent,
@@ -53,7 +52,6 @@ import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-w
53
52
  import {
54
53
  attachCursorSdkEventDebugPiStreamTap,
55
54
  CursorSdkEventDebugSink,
56
- DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
57
55
  } from "./cursor-sdk-event-debug.js";
58
56
  import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
59
57
  import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display.js";
@@ -71,6 +69,10 @@ import {
71
69
  loadCursorTranscriptWebToolCallsAfterOffset,
72
70
  } from "./cursor-agent-message-web-tools.js";
73
71
  import { installCursorSdkAbortErrorSuppression } from "./cursor-sdk-abort-error-guard.js";
72
+ import {
73
+ buildIncompleteCursorToolRunOutcome,
74
+ type IncompleteCursorToolRunOutcomeInput,
75
+ } from "./cursor-incomplete-tool-visibility.js";
74
76
 
75
77
  function makeInitialMessage(model: Model<Api>): AssistantMessage {
76
78
  return {
@@ -112,6 +114,21 @@ async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<
112
114
  }
113
115
  }
114
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
+
115
132
  export function streamCursor(
116
133
  model: Model<Api>,
117
134
  context: Context,
@@ -364,6 +381,7 @@ export function streamCursor(
364
381
  throw new CursorLiveRunAbortError();
365
382
  }
366
383
  const activeRun = run;
384
+ const activeSessionAgentLease = sessionAgentLease;
367
385
 
368
386
  if (liveRun) {
369
387
  deferSdkEventDebugFinalize = true;
@@ -371,26 +389,26 @@ export function streamCursor(
371
389
  .wait()
372
390
  .then(async (result) => {
373
391
  sdkEventDebug?.recordWaitResult(result);
374
- if (result.status === "finished" && !options?.signal?.aborted) {
392
+ const finishedSuccessfully = result.status === "finished" && !options?.signal?.aborted;
393
+ if (finishedSuccessfully) {
375
394
  await replayCursorTranscriptWebToolCalls(activeRun.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
376
395
  }
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
- );
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
+ });
384
405
  await sdkEventDebug?.captureRunArtifacts(run);
385
406
  if (liveRun.disposed) return;
386
407
  await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
387
408
  if (liveRun.disposed) return;
388
- if (result.status === "finished" && !options?.signal?.aborted) {
389
- commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
390
- cursorLiveRuns.markFinished(
391
- liveRun,
392
- selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, turnCoordinator.planTextCandidate),
393
- );
409
+ if (finishedSuccessfully) {
410
+ activeSessionAgentLease.commitSend(context, bootstrap);
411
+ cursorLiveRuns.markFinished(liveRun, finalCursorText);
394
412
  } else if (result.status === "cancelled" || options?.signal?.aborted) {
395
413
  cursorLiveRuns.markCancelled(
396
414
  liveRun,
@@ -412,7 +430,7 @@ export function streamCursor(
412
430
  .catch(async (error: unknown) => {
413
431
  sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) });
414
432
  sdkEventDebug?.recordError("run_wait", error);
415
- turnCoordinatorForCleanup?.discardIncompleteStartedToolCalls("sdk-failure");
433
+ discardIncompleteToolsForRunOutcome(turnCoordinatorForCleanup, { status: "error" });
416
434
  await sdkEventDebug?.captureRunArtifacts(run);
417
435
  if (liveRun.disposed) return;
418
436
  cursorLiveRuns.markError(liveRun, sanitizeCursorProviderError(error, resolvedApiKey ?? options?.apiKey));
@@ -432,7 +450,7 @@ export function streamCursor(
432
450
  } catch (error) {
433
451
  if (error instanceof CursorLiveRunAbortError) {
434
452
  sdkAbortErrorSuppression.suppressAbortErrors();
435
- turnCoordinator.discardIncompleteStartedToolCalls("abort");
453
+ discardIncompleteToolsForRunOutcome(turnCoordinator, { status: "cancelled", signalAborted: true });
436
454
  turnCoordinator.closeTraceBlock();
437
455
  flushPendingCursorLiveRunTraceEventsToStream(stream, partial, liveRun, {
438
456
  includeTracesBehindQueuedTools: true,
@@ -442,6 +460,7 @@ export function streamCursor(
442
460
  throw error;
443
461
  } finally {
444
462
  sdkEventDebugRef.current = undefined;
463
+ activeSessionAgentLease.trackRunCompletion(waitCompletion);
445
464
  void waitCompletion
446
465
  .finally(async () => {
447
466
  try {
@@ -459,16 +478,21 @@ export function streamCursor(
459
478
 
460
479
  const result = await run.wait();
461
480
  sdkEventDebug?.recordWaitResult(result);
462
- if (result.status === "finished" && !options?.signal?.aborted) {
481
+ const finishedSuccessfully = result.status === "finished" && !options?.signal?.aborted;
482
+ if (finishedSuccessfully) {
463
483
  await replayCursorTranscriptWebToolCalls(run.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
464
484
  }
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
- );
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
+ });
472
496
  await sdkEventDebug?.captureRunArtifacts(run);
473
497
  await cacheSdkContextWindow(agent.agentId, model.id);
474
498
 
@@ -493,19 +517,17 @@ export function streamCursor(
493
517
  partial.errorMessage = sanitizeCursorProviderError(failureDetail, resolvedApiKey ?? options?.apiKey);
494
518
  stream.push({ type: "error", reason: "error", error: partial });
495
519
  } else {
496
- commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
497
- const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""), turnCoordinator.planTextCandidate, {
498
- allowPartialPrefix: true,
499
- });
520
+ sessionAgentLease.commitSend(context, bootstrap);
500
521
  turnCoordinator.flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
501
522
  applyCursorApproximateUsage(partial, model, context, promptInputTokens);
502
523
  stream.push({ type: "done", reason: "stop", message: partial });
503
524
  }
504
525
  } catch (error) {
505
526
  sdkEventDebug?.recordError("provider_stream", error);
506
- turnCoordinatorForCleanup?.discardIncompleteStartedToolCalls(
507
- error instanceof CursorLiveRunAbortError ? "abort" : "sdk-failure",
508
- );
527
+ discardIncompleteToolsForRunOutcome(turnCoordinatorForCleanup, {
528
+ status: error instanceof CursorLiveRunAbortError ? "cancelled" : "error",
529
+ signalAborted: error instanceof CursorLiveRunAbortError,
530
+ });
509
531
  if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun);
510
532
  else await abandonSessionCursorAgent(sessionAgentScopeKey);
511
533
  if (error instanceof CursorLiveRunAbortError) {
@@ -35,7 +35,12 @@ export {
35
35
  resolveCursorSdkEventDebugBaseDir,
36
36
  } from "./cursor-sdk-event-debug-constants.js";
37
37
 
38
- export type CursorSdkDisplayDecisionAction = "skip-duplicate" | "queue_replay" | "emit_trace" | "ignore-bridge";
38
+ export type CursorSdkDisplayDecisionAction =
39
+ | "skip-duplicate"
40
+ | "skip-incomplete-fast-local"
41
+ | "queue_replay"
42
+ | "emit_trace"
43
+ | "ignore-bridge";
39
44
 
40
45
  export interface CursorSdkDisplayDecisionRecord {
41
46
  action: CursorSdkDisplayDecisionAction;