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.
- package/CHANGELOG.md +20 -0
- package/README.md +23 -2
- package/docs/cursor-live-smoke-checklist.md +1 -1
- package/docs/cursor-model-ux-spec.md +5 -4
- package/docs/cursor-native-tool-replay.md +6 -4
- package/docs/cursor-testing-lessons.md +2 -2
- package/package.json +4 -2
- package/scripts/probe-mcp-coldstart.mjs +244 -0
- package/src/cursor-incomplete-tool-visibility.ts +51 -45
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-native-tool-display-replay.ts +2 -1
- package/src/cursor-provider-turn-coordinator.ts +29 -8
- package/src/cursor-provider.ts +55 -33
- package/src/cursor-sdk-event-debug.ts +6 -1
- package/src/cursor-session-agent.ts +262 -87
- package/src/cursor-tool-lifecycle.ts +9 -35
- package/src/cursor-tool-names.ts +27 -0
- package/src/cursor-tool-visibility.ts +63 -0
- package/src/cursor-transcript-tool-specs.ts +26 -14
|
@@ -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 {
|
|
13
|
-
import {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
53
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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:
|
|
139
|
+
timeoutMs: installedToolTimeoutMs,
|
|
140
|
+
connectTimeoutMs: installedConnectTimeoutMs,
|
|
93
141
|
sdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
|
|
94
142
|
};
|
|
95
143
|
}
|
|
96
144
|
|
|
97
|
-
export function
|
|
145
|
+
export function restoreCursorMcpToolTimeoutOverride(): void {
|
|
98
146
|
if (originalSetTimeout) {
|
|
99
147
|
globalThis.setTimeout = originalSetTimeout;
|
|
100
148
|
originalSetTimeout = undefined;
|
|
101
149
|
}
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
178
|
+
toolName,
|
|
171
179
|
callId,
|
|
172
|
-
reason,
|
|
180
|
+
reason: outcome.reason,
|
|
173
181
|
});
|
|
174
|
-
|
|
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:
|
|
573
|
+
toolName: getNormalizedCursorToolName(toolCall),
|
|
553
574
|
progressText,
|
|
554
575
|
liveRun: this.liveRun !== undefined,
|
|
555
576
|
});
|
package/src/cursor-provider.ts
CHANGED
|
@@ -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
|
-
|
|
392
|
+
const finishedSuccessfully = result.status === "finished" && !options?.signal?.aborted;
|
|
393
|
+
if (finishedSuccessfully) {
|
|
375
394
|
await replayCursorTranscriptWebToolCalls(activeRun.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
|
|
376
395
|
}
|
|
377
|
-
|
|
378
|
-
result.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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 (
|
|
389
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
481
|
+
const finishedSuccessfully = result.status === "finished" && !options?.signal?.aborted;
|
|
482
|
+
if (finishedSuccessfully) {
|
|
463
483
|
await replayCursorTranscriptWebToolCalls(run.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
|
|
464
484
|
}
|
|
465
|
-
|
|
466
|
-
result.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
|
507
|
-
error instanceof CursorLiveRunAbortError ? "
|
|
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 =
|
|
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;
|