pi-cursor-sdk 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +37 -0
- package/docs/cursor-live-smoke-checklist.md +3 -0
- package/docs/cursor-model-ux-spec.md +4 -3
- package/docs/cursor-native-tool-replay.md +96 -2
- package/docs/cursor-testing-lessons.md +234 -5
- package/package.json +8 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/validate-smoke-jsonl.mjs +27 -3
- package/src/context.ts +45 -32
- package/src/cursor-agent-message-web-tools.ts +172 -0
- package/src/cursor-agents-context.ts +176 -0
- package/src/cursor-incomplete-tool-visibility.ts +118 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-tool-display-registration.ts +1 -4
- package/src/cursor-native-tool-display-replay.ts +63 -5
- package/src/cursor-native-tool-display-tools.ts +20 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
- package/src/cursor-pi-tool-bridge-run.ts +16 -1
- package/src/cursor-pi-tool-bridge-types.ts +3 -0
- package/src/cursor-provider-errors.ts +96 -0
- package/src/cursor-provider-live-run-drain.ts +181 -62
- package/src/cursor-provider-turn-coordinator.ts +198 -32
- package/src/cursor-provider.ts +270 -83
- package/src/cursor-question-tool.ts +1 -4
- package/src/cursor-sdk-abort-error-guard.ts +109 -0
- package/src/cursor-sdk-event-debug-constants.ts +40 -0
- package/src/cursor-sdk-event-debug-session.ts +163 -0
- package/src/cursor-sdk-event-debug.ts +597 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +25 -3
- package/src/cursor-session-send-policy.ts +43 -0
- package/src/cursor-setting-sources.ts +29 -0
- package/src/cursor-state.ts +1 -5
- package/src/cursor-tool-lifecycle.ts +111 -0
- package/src/cursor-tool-names.ts +12 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +113 -14
- package/src/cursor-transcript-utils.ts +12 -0
- package/src/cursor-web-tool-activity.ts +84 -0
- package/src/index.ts +4 -1
package/src/cursor-provider.ts
CHANGED
|
@@ -8,14 +8,15 @@ import {
|
|
|
8
8
|
type AssistantMessage,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
10
|
import { Agent, createAgentPlatform } from "@cursor/sdk";
|
|
11
|
-
import type { SDKAgent
|
|
11
|
+
import type { SDKAgent } from "@cursor/sdk";
|
|
12
12
|
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
13
13
|
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
|
|
14
|
-
import { buildCursorSendPrompt } from "./context.js";
|
|
15
14
|
import {
|
|
16
15
|
acquireSessionCursorAgent,
|
|
16
|
+
buildCursorSessionSendPrompt,
|
|
17
17
|
commitSessionAgentSend,
|
|
18
18
|
disposeAllSessionCursorAgents,
|
|
19
|
+
planCursorSessionSend,
|
|
19
20
|
resetSessionCursorAgent,
|
|
20
21
|
} from "./cursor-session-agent.js";
|
|
21
22
|
import {
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
cursorLiveRuns,
|
|
37
38
|
drainCursorLiveRunTurn,
|
|
38
39
|
drainExistingCursorLiveRunBeforeSend,
|
|
40
|
+
flushPendingCursorLiveRunTraceEventsToStream,
|
|
39
41
|
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
40
42
|
getPendingCursorLiveRun,
|
|
41
43
|
hasTrailingUserMessagesAfterToolResults,
|
|
@@ -48,8 +50,27 @@ import {
|
|
|
48
50
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
49
51
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
50
52
|
import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
|
|
53
|
+
import {
|
|
54
|
+
attachCursorSdkEventDebugPiStreamTap,
|
|
55
|
+
CursorSdkEventDebugSink,
|
|
56
|
+
DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
|
|
57
|
+
} from "./cursor-sdk-event-debug.js";
|
|
51
58
|
import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
52
59
|
import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display.js";
|
|
60
|
+
import {
|
|
61
|
+
formatCursorSdkAbortMessage,
|
|
62
|
+
formatCursorSdkRunFailureDetail,
|
|
63
|
+
MISSING_CURSOR_API_KEY_MESSAGE,
|
|
64
|
+
resolveCursorSdkAbortCause,
|
|
65
|
+
sanitizeCursorProviderError,
|
|
66
|
+
} from "./cursor-provider-errors.js";
|
|
67
|
+
import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
|
|
68
|
+
import { hasUsableText } from "./cursor-record-utils.js";
|
|
69
|
+
import {
|
|
70
|
+
countCursorAgentMessages,
|
|
71
|
+
loadCursorTranscriptWebToolCallsAfterOffset,
|
|
72
|
+
} from "./cursor-agent-message-web-tools.js";
|
|
73
|
+
import { installCursorSdkAbortErrorSuppression } from "./cursor-sdk-abort-error-guard.js";
|
|
53
74
|
|
|
54
75
|
function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
55
76
|
return {
|
|
@@ -72,25 +93,6 @@ function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
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
96
|
|
|
95
97
|
function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
96
98
|
const trimmed = apiKey?.trim();
|
|
@@ -99,27 +101,6 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
|
99
101
|
return trimmed;
|
|
100
102
|
}
|
|
101
103
|
|
|
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
104
|
async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<void> {
|
|
124
105
|
try {
|
|
125
106
|
const platform = await createAgentPlatform();
|
|
@@ -137,6 +118,8 @@ export function streamCursor(
|
|
|
137
118
|
options?: SimpleStreamOptions,
|
|
138
119
|
): AssistantMessageEventStream {
|
|
139
120
|
const stream = createAssistantMessageEventStream();
|
|
121
|
+
const sdkEventDebugRef: { current?: CursorSdkEventDebugSink } = {};
|
|
122
|
+
attachCursorSdkEventDebugPiStreamTap(stream, sdkEventDebugRef);
|
|
140
123
|
|
|
141
124
|
(async () => {
|
|
142
125
|
const partial = makeInitialMessage(model);
|
|
@@ -150,8 +133,51 @@ export function streamCursor(
|
|
|
150
133
|
let abortSignal: AbortSignal | undefined;
|
|
151
134
|
let abortListener: (() => void) | undefined;
|
|
152
135
|
let restoreCursorSdkOutputFilter: (() => void) | undefined;
|
|
136
|
+
let sdkEventDebug: CursorSdkEventDebugSink | undefined;
|
|
137
|
+
let deferSdkEventDebugFinalize = false;
|
|
138
|
+
const sdkAbortErrorSuppression = installCursorSdkAbortErrorSuppression();
|
|
139
|
+
|
|
140
|
+
const pushSanitizedStreamError = (error: unknown, reason: "error" | "aborted" = "error"): void => {
|
|
141
|
+
partial.stopReason = reason;
|
|
142
|
+
partial.errorMessage =
|
|
143
|
+
reason === "aborted"
|
|
144
|
+
? formatCursorSdkAbortMessage(resolveCursorSdkAbortCause({ signalAborted: options?.signal?.aborted }))
|
|
145
|
+
: sanitizeCursorProviderError(error, resolvedApiKey ?? options?.apiKey);
|
|
146
|
+
stream.push({ type: "error", reason, error: partial });
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const getCursorAgentMessageOffset = async (agentId: string, cwd: string): Promise<number | undefined> => {
|
|
150
|
+
try {
|
|
151
|
+
return await countCursorAgentMessages(agentId, cwd);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
sdkEventDebug?.recordError("cursor_agent_message_count", error);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const replayCursorTranscriptWebToolCalls = async (
|
|
159
|
+
agentId: string,
|
|
160
|
+
cwd: string,
|
|
161
|
+
messageOffset: number | undefined,
|
|
162
|
+
turnCoordinator: CursorSdkTurnCoordinator,
|
|
163
|
+
): Promise<void> => {
|
|
164
|
+
try {
|
|
165
|
+
const transcriptToolCalls = await loadCursorTranscriptWebToolCallsAfterOffset({ agentId, cwd, offset: messageOffset });
|
|
166
|
+
if (transcriptToolCalls.length === 0) return;
|
|
167
|
+
sdkEventDebug?.recordCoordinatorEvent("cursor-transcript-web-tools", {
|
|
168
|
+
agentId,
|
|
169
|
+
messageOffset,
|
|
170
|
+
count: transcriptToolCalls.length,
|
|
171
|
+
});
|
|
172
|
+
turnCoordinator.handleTranscriptCompletedToolCalls(transcriptToolCalls);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
sdkEventDebug?.recordError("cursor_transcript_web_tools", error);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
153
177
|
|
|
154
178
|
try {
|
|
179
|
+
let turnCoordinatorForCleanup: CursorSdkTurnCoordinator | undefined;
|
|
180
|
+
try {
|
|
155
181
|
const throwIfAborted = (): void => {
|
|
156
182
|
if (options?.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
157
183
|
};
|
|
@@ -159,21 +185,29 @@ export function streamCursor(
|
|
|
159
185
|
stream.push({ type: "start", partial });
|
|
160
186
|
throwIfAborted();
|
|
161
187
|
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
const cwd = getCursorSessionCwd();
|
|
189
|
+
sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
|
|
190
|
+
cwd,
|
|
191
|
+
modelId: model.id,
|
|
192
|
+
provider: model.provider,
|
|
193
|
+
});
|
|
194
|
+
sdkEventDebugRef.current = sdkEventDebug;
|
|
195
|
+
sdkEventDebug?.recordContextSnapshot(context);
|
|
196
|
+
|
|
197
|
+
if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, sdkEventDebug)) === "stream_ended") {
|
|
198
|
+
sdkEventDebug?.recordFinalPartial(partial);
|
|
199
|
+
await sdkEventDebug?.finalize();
|
|
200
|
+
sdkEventDebugRef.current = undefined;
|
|
164
201
|
return;
|
|
165
202
|
}
|
|
166
203
|
|
|
167
204
|
const apiKey = resolveCursorApiKey(options?.apiKey);
|
|
168
|
-
if (!apiKey) throw new Error(
|
|
205
|
+
if (!apiKey) throw new Error(MISSING_CURSOR_API_KEY_MESSAGE);
|
|
169
206
|
resolvedApiKey = apiKey;
|
|
170
207
|
|
|
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
208
|
const fastEnabled = getEffectiveFastForModelId(model.id);
|
|
175
209
|
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
176
|
-
const settingSources =
|
|
210
|
+
const settingSources = getEffectiveCursorSettingSources();
|
|
177
211
|
|
|
178
212
|
installCursorMcpToolTimeoutOverride();
|
|
179
213
|
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
@@ -182,6 +216,7 @@ export function streamCursor(
|
|
|
182
216
|
cwd,
|
|
183
217
|
modelSelection: selection,
|
|
184
218
|
settingSources,
|
|
219
|
+
debugRecorder: sdkEventDebug,
|
|
185
220
|
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
186
221
|
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
187
222
|
cursorLiveRuns.queueEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
@@ -199,19 +234,39 @@ export function streamCursor(
|
|
|
199
234
|
throwIfAborted();
|
|
200
235
|
|
|
201
236
|
const promptOptions = getCursorPromptOptions(model);
|
|
202
|
-
let
|
|
203
|
-
|
|
237
|
+
let sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
|
|
238
|
+
let prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
|
|
239
|
+
if (sendPlan.resetAgent) {
|
|
204
240
|
await resetSessionCursorAgent(sessionAgentLease.scopeKey);
|
|
205
241
|
sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
206
242
|
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
207
243
|
agent = sessionAgentLease.agent;
|
|
208
244
|
bridgeRun = sessionAgentLease.bridgeRun;
|
|
209
|
-
|
|
245
|
+
sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
|
|
246
|
+
prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
|
|
210
247
|
}
|
|
248
|
+
const bootstrap = sendPlan.mode === "bootstrap";
|
|
211
249
|
const sessionBridgeRun = sessionAgentLease.bridgeRun;
|
|
212
250
|
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
213
251
|
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
214
252
|
const activeToolNames = getActiveContextToolNames(context);
|
|
253
|
+
sdkEventDebug?.recordProviderMeta({
|
|
254
|
+
model: {
|
|
255
|
+
id: model.id,
|
|
256
|
+
provider: model.provider,
|
|
257
|
+
api: model.api,
|
|
258
|
+
reasoning: options?.reasoning ?? "off",
|
|
259
|
+
fastEnabled,
|
|
260
|
+
selection,
|
|
261
|
+
},
|
|
262
|
+
settingSources: settingSources ?? null,
|
|
263
|
+
sendState: sessionAgentLease.sendState,
|
|
264
|
+
sendPlan,
|
|
265
|
+
promptOptions,
|
|
266
|
+
activeToolNames: activeToolNames ? [...activeToolNames] : [],
|
|
267
|
+
sessionAgentScopeKey,
|
|
268
|
+
bridgeRunId: bridgeRun?.id,
|
|
269
|
+
});
|
|
215
270
|
const nativeReplayId = createCursorNativeReplayId();
|
|
216
271
|
const textDeltas: string[] = [];
|
|
217
272
|
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
@@ -224,6 +279,7 @@ export function streamCursor(
|
|
|
224
279
|
sessionAgentScopeKey,
|
|
225
280
|
promptInputTokens,
|
|
226
281
|
textDeltas,
|
|
282
|
+
debugRecorder: sdkEventDebug,
|
|
227
283
|
})
|
|
228
284
|
: undefined;
|
|
229
285
|
if (liveRun) {
|
|
@@ -243,11 +299,14 @@ export function streamCursor(
|
|
|
243
299
|
activeToolNames,
|
|
244
300
|
nativeReplayId,
|
|
245
301
|
textDeltas,
|
|
302
|
+
debugRecorder: sdkEventDebug,
|
|
246
303
|
});
|
|
304
|
+
turnCoordinatorForCleanup = turnCoordinator;
|
|
247
305
|
|
|
248
306
|
// Handle abort signal
|
|
249
307
|
let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
|
|
250
308
|
abortListener = () => {
|
|
309
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
251
310
|
activeLiveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
252
311
|
if (run) {
|
|
253
312
|
run.cancel().catch(() => {});
|
|
@@ -257,25 +316,73 @@ export function streamCursor(
|
|
|
257
316
|
abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
258
317
|
|
|
259
318
|
throwIfAborted();
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
319
|
+
sdkEventDebug?.recordSendMeta({
|
|
320
|
+
mode: sendPlan.mode,
|
|
321
|
+
reason: sendPlan.reason,
|
|
322
|
+
resetAgent: sendPlan.resetAgent,
|
|
323
|
+
bootstrap,
|
|
324
|
+
promptText: prompt.text,
|
|
325
|
+
imageCount: prompt.images.length,
|
|
326
|
+
useNativeToolReplay,
|
|
327
|
+
bridgeEnabled: bridgeRun !== undefined,
|
|
328
|
+
nativeReplayId,
|
|
329
|
+
promptInputTokens,
|
|
330
|
+
});
|
|
331
|
+
const sendPayload = {
|
|
332
|
+
text: prompt.text,
|
|
333
|
+
images: prompt.images.length > 0 ? prompt.images : undefined,
|
|
334
|
+
};
|
|
335
|
+
sdkEventDebug?.recordSendPayload(sendPayload);
|
|
336
|
+
const cursorAgentMessageOffset = await getCursorAgentMessageOffset(agent.agentId, cwd);
|
|
337
|
+
sdkEventDebug?.recordProviderEvent("agent_send_start", sendPayload);
|
|
338
|
+
run = await agent.send(sendPayload, {
|
|
339
|
+
onDelta: (args) => {
|
|
340
|
+
sdkEventDebug?.recordOnDelta(args.update);
|
|
341
|
+
turnCoordinator.handleDelta(args.update);
|
|
265
342
|
},
|
|
266
|
-
|
|
343
|
+
onStep: (args) => {
|
|
344
|
+
sdkEventDebug?.recordOnStep(args.step);
|
|
345
|
+
turnCoordinator.handleStep(args.step);
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
sdkEventDebug?.recordRunMeta({
|
|
349
|
+
runId: run.id,
|
|
350
|
+
agentId: run.agentId,
|
|
351
|
+
status: run.status,
|
|
352
|
+
});
|
|
353
|
+
sdkEventDebug?.attachRunStream(run);
|
|
354
|
+
sdkEventDebug?.recordProviderEvent("agent_send_returned", {
|
|
355
|
+
runId: run.id,
|
|
356
|
+
agentId: run.agentId,
|
|
357
|
+
status: run.status,
|
|
358
|
+
});
|
|
267
359
|
if (liveRun) cursorLiveRuns.attachSdkRun(liveRun, run);
|
|
268
360
|
if (options?.signal?.aborted) {
|
|
361
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
362
|
+
liveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
269
363
|
await run.cancel().catch(() => {});
|
|
270
364
|
throw new CursorLiveRunAbortError();
|
|
271
365
|
}
|
|
366
|
+
const activeRun = run;
|
|
272
367
|
|
|
273
368
|
if (liveRun) {
|
|
274
|
-
|
|
369
|
+
deferSdkEventDebugFinalize = true;
|
|
370
|
+
const waitCompletion = run
|
|
275
371
|
.wait()
|
|
276
372
|
.then(async (result) => {
|
|
373
|
+
sdkEventDebug?.recordWaitResult(result);
|
|
374
|
+
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
375
|
+
await replayCursorTranscriptWebToolCalls(activeRun.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
|
|
376
|
+
}
|
|
377
|
+
turnCoordinator.discardIncompleteStartedToolCalls(
|
|
378
|
+
result.status === "cancelled" || options?.signal?.aborted
|
|
379
|
+
? "abort"
|
|
380
|
+
: result.status === "error"
|
|
381
|
+
? "sdk-failure"
|
|
382
|
+
: DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
|
|
383
|
+
);
|
|
384
|
+
await sdkEventDebug?.captureRunArtifacts(run);
|
|
277
385
|
if (liveRun.disposed) return;
|
|
278
|
-
turnCoordinator.discardIncompleteStartedToolCalls();
|
|
279
386
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
280
387
|
if (liveRun.disposed) return;
|
|
281
388
|
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
@@ -285,14 +392,30 @@ export function streamCursor(
|
|
|
285
392
|
selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, turnCoordinator.planTextCandidate),
|
|
286
393
|
);
|
|
287
394
|
} else if (result.status === "cancelled" || options?.signal?.aborted) {
|
|
288
|
-
cursorLiveRuns.markCancelled(
|
|
395
|
+
cursorLiveRuns.markCancelled(
|
|
396
|
+
liveRun,
|
|
397
|
+
formatCursorSdkAbortMessage(
|
|
398
|
+
resolveCursorSdkAbortCause({
|
|
399
|
+
signalAborted: options?.signal?.aborted,
|
|
400
|
+
sdkStatusCancelled: result.status === "cancelled",
|
|
401
|
+
}),
|
|
402
|
+
),
|
|
403
|
+
);
|
|
289
404
|
} else {
|
|
290
|
-
|
|
405
|
+
const failureDetail = formatCursorSdkRunFailureDetail(result, run?.result);
|
|
406
|
+
cursorLiveRuns.markError(
|
|
407
|
+
liveRun,
|
|
408
|
+
sanitizeCursorProviderError(failureDetail, resolvedApiKey ?? options?.apiKey),
|
|
409
|
+
);
|
|
291
410
|
}
|
|
292
411
|
})
|
|
293
412
|
.catch(async (error: unknown) => {
|
|
413
|
+
sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) });
|
|
414
|
+
sdkEventDebug?.recordError("run_wait", error);
|
|
415
|
+
turnCoordinatorForCleanup?.discardIncompleteStartedToolCalls("sdk-failure");
|
|
416
|
+
await sdkEventDebug?.captureRunArtifacts(run);
|
|
294
417
|
if (liveRun.disposed) return;
|
|
295
|
-
cursorLiveRuns.markError(liveRun,
|
|
418
|
+
cursorLiveRuns.markError(liveRun, sanitizeCursorProviderError(error, resolvedApiKey ?? options?.apiKey));
|
|
296
419
|
});
|
|
297
420
|
|
|
298
421
|
try {
|
|
@@ -300,18 +423,53 @@ export function streamCursor(
|
|
|
300
423
|
await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
|
|
301
424
|
await settleCursorLiveToolBatch(liveRun);
|
|
302
425
|
turnCoordinator.closeTraceBlock();
|
|
303
|
-
await drainCursorLiveRunTurn(stream, partial, model, context, liveRun, 0, {
|
|
426
|
+
await drainCursorLiveRunTurn(stream, partial, model, context, liveRun, 0, {
|
|
427
|
+
mode: "emit",
|
|
428
|
+
signal: options?.signal,
|
|
429
|
+
debugRecorder: sdkEventDebug,
|
|
430
|
+
});
|
|
304
431
|
});
|
|
305
432
|
} catch (error) {
|
|
306
|
-
if (error instanceof CursorLiveRunAbortError)
|
|
433
|
+
if (error instanceof CursorLiveRunAbortError) {
|
|
434
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
435
|
+
turnCoordinator.discardIncompleteStartedToolCalls("abort");
|
|
436
|
+
turnCoordinator.closeTraceBlock();
|
|
437
|
+
flushPendingCursorLiveRunTraceEventsToStream(stream, partial, liveRun, {
|
|
438
|
+
includeTracesBehindQueuedTools: true,
|
|
439
|
+
});
|
|
440
|
+
await cursorLiveRuns.release(liveRun);
|
|
441
|
+
}
|
|
307
442
|
throw error;
|
|
443
|
+
} finally {
|
|
444
|
+
sdkEventDebugRef.current = undefined;
|
|
445
|
+
void waitCompletion
|
|
446
|
+
.finally(async () => {
|
|
447
|
+
try {
|
|
448
|
+
sdkEventDebug?.recordFinalPartial(partial);
|
|
449
|
+
await sdkEventDebug?.finalize();
|
|
450
|
+
} finally {
|
|
451
|
+
sdkAbortErrorSuppression.dispose();
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
.catch(() => {});
|
|
308
455
|
}
|
|
309
456
|
agent = null;
|
|
310
457
|
return;
|
|
311
458
|
}
|
|
312
459
|
|
|
313
460
|
const result = await run.wait();
|
|
314
|
-
|
|
461
|
+
sdkEventDebug?.recordWaitResult(result);
|
|
462
|
+
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
463
|
+
await replayCursorTranscriptWebToolCalls(run.agentId, cwd, cursorAgentMessageOffset, turnCoordinator);
|
|
464
|
+
}
|
|
465
|
+
turnCoordinator.discardIncompleteStartedToolCalls(
|
|
466
|
+
result.status === "cancelled" || options?.signal?.aborted
|
|
467
|
+
? "abort"
|
|
468
|
+
: result.status === "error"
|
|
469
|
+
? "sdk-failure"
|
|
470
|
+
: DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
|
|
471
|
+
);
|
|
472
|
+
await sdkEventDebug?.captureRunArtifacts(run);
|
|
315
473
|
await cacheSdkContextWindow(agent.agentId, model.id);
|
|
316
474
|
|
|
317
475
|
// Close any open thinking/activity trace, then use the final run result only when
|
|
@@ -321,11 +479,18 @@ export function streamCursor(
|
|
|
321
479
|
if (result.status === "cancelled") {
|
|
322
480
|
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
323
481
|
partial.stopReason = "aborted";
|
|
482
|
+
partial.errorMessage = formatCursorSdkAbortMessage(
|
|
483
|
+
resolveCursorSdkAbortCause({
|
|
484
|
+
signalAborted: options?.signal?.aborted,
|
|
485
|
+
sdkStatusCancelled: true,
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
324
488
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
325
489
|
} else if (result.status === "error") {
|
|
326
490
|
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
327
491
|
partial.stopReason = "error";
|
|
328
|
-
|
|
492
|
+
const failureDetail = formatCursorSdkRunFailureDetail(result, run.result);
|
|
493
|
+
partial.errorMessage = sanitizeCursorProviderError(failureDetail, resolvedApiKey ?? options?.apiKey);
|
|
329
494
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
330
495
|
} else {
|
|
331
496
|
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
@@ -336,27 +501,49 @@ export function streamCursor(
|
|
|
336
501
|
applyCursorApproximateUsage(partial, model, context, promptInputTokens);
|
|
337
502
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
338
503
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
504
|
+
} catch (error) {
|
|
505
|
+
sdkEventDebug?.recordError("provider_stream", error);
|
|
506
|
+
turnCoordinatorForCleanup?.discardIncompleteStartedToolCalls(
|
|
507
|
+
error instanceof CursorLiveRunAbortError ? "abort" : "sdk-failure",
|
|
508
|
+
);
|
|
509
|
+
if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun);
|
|
510
|
+
else await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
511
|
+
if (error instanceof CursorLiveRunAbortError) {
|
|
512
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
513
|
+
pushSanitizedStreamError(error, "aborted");
|
|
514
|
+
} else {
|
|
515
|
+
pushSanitizedStreamError(error, "error");
|
|
516
|
+
}
|
|
517
|
+
} finally {
|
|
518
|
+
if (!deferSdkEventDebugFinalize) {
|
|
519
|
+
try {
|
|
520
|
+
sdkEventDebug?.recordFinalPartial(partial);
|
|
521
|
+
await sdkEventDebug?.finalize();
|
|
522
|
+
} finally {
|
|
523
|
+
sdkAbortErrorSuppression.dispose();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
sdkEventDebugRef.current = undefined;
|
|
527
|
+
restoreCursorSdkOutputFilter?.();
|
|
352
528
|
|
|
353
|
-
|
|
354
|
-
|
|
529
|
+
if (abortSignal && abortListener) {
|
|
530
|
+
abortSignal.removeEventListener("abort", abortListener);
|
|
531
|
+
}
|
|
355
532
|
}
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun).catch(() => {});
|
|
535
|
+
else await abandonSessionCursorAgent(sessionAgentScopeKey).catch(() => {});
|
|
536
|
+
pushSanitizedStreamError(error, error instanceof CursorLiveRunAbortError ? "aborted" : "error");
|
|
356
537
|
}
|
|
357
538
|
|
|
358
539
|
stream.end();
|
|
359
|
-
})()
|
|
540
|
+
})().catch((error: unknown) => {
|
|
541
|
+
const partial = makeInitialMessage(model);
|
|
542
|
+
partial.stopReason = "error";
|
|
543
|
+
partial.errorMessage = sanitizeCursorProviderError(error, resolveCursorApiKey(options?.apiKey));
|
|
544
|
+
stream.push({ type: "error", reason: "error", error: partial });
|
|
545
|
+
stream.end();
|
|
546
|
+
});
|
|
360
547
|
|
|
361
548
|
return stream;
|
|
362
549
|
}
|
|
@@ -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();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
2
|
+
|
|
3
|
+
interface CursorSdkAbortErrorSuppressionToken {
|
|
4
|
+
suppress: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CursorSdkAbortErrorSuppression {
|
|
8
|
+
suppressAbortErrors(): void;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
13
|
+
const value = record?.[key];
|
|
14
|
+
return typeof value === "string" ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type GenericProcessEmit = (event: string | symbol, ...args: unknown[]) => boolean;
|
|
18
|
+
|
|
19
|
+
// The local Cursor SDK can surface abort-time ConnectRPC cancellation as a process-level
|
|
20
|
+
// uncaught exception/unhandled rejection even when run.cancel() is awaited/caught.
|
|
21
|
+
const activeSuppressions = new Set<CursorSdkAbortErrorSuppressionToken>();
|
|
22
|
+
let originalProcessEmit: GenericProcessEmit | undefined;
|
|
23
|
+
let captureCallbackInstalled = false;
|
|
24
|
+
|
|
25
|
+
export function isCursorSdkAbortConnectError(error: unknown): boolean {
|
|
26
|
+
const record = asRecord(error);
|
|
27
|
+
const name = error instanceof Error ? error.name : getString(record, "name");
|
|
28
|
+
const message = error instanceof Error ? error.message : getString(record, "message");
|
|
29
|
+
const rawMessage = getString(record, "rawMessage") ?? message;
|
|
30
|
+
const code = record?.code;
|
|
31
|
+
const cause = asRecord(record?.cause);
|
|
32
|
+
const causeName = getString(cause, "name");
|
|
33
|
+
const stack = error instanceof Error ? error.stack ?? "" : getString(record, "stack") ?? "";
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
name === "ConnectError" &&
|
|
37
|
+
(code === 1 || code === "canceled") &&
|
|
38
|
+
Boolean(rawMessage && /(?:operation was aborted|canceled)/i.test(rawMessage)) &&
|
|
39
|
+
(causeName === "AbortError" || /AbortError/.test(stack)) &&
|
|
40
|
+
stack.includes("@cursor/sdk") &&
|
|
41
|
+
stack.includes("@connectrpc/connect-node")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasActiveSuppression(): boolean {
|
|
46
|
+
for (const suppression of activeSuppressions) {
|
|
47
|
+
if (suppression.suppress) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldSuppressProcessError(event: string | symbol, args: readonly unknown[]): boolean {
|
|
53
|
+
if (event !== "uncaughtException" && event !== "unhandledRejection") return false;
|
|
54
|
+
return hasActiveSuppression() && isCursorSdkAbortConnectError(args[0]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function installProcessEmitPatch(): void {
|
|
58
|
+
if (originalProcessEmit) return;
|
|
59
|
+
originalProcessEmit = process.emit.bind(process) as GenericProcessEmit;
|
|
60
|
+
process.emit = function patchedCursorSdkAbortEmit(this: NodeJS.Process, event: string | symbol, ...args: unknown[]): boolean {
|
|
61
|
+
if (shouldSuppressProcessError(event, args)) return false;
|
|
62
|
+
return originalProcessEmit!(event, ...args);
|
|
63
|
+
} as typeof process.emit;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function installCaptureCallbackIfAvailable(): void {
|
|
67
|
+
if (captureCallbackInstalled || process.hasUncaughtExceptionCaptureCallback()) return;
|
|
68
|
+
process.setUncaughtExceptionCaptureCallback((error: Error) => {
|
|
69
|
+
if (shouldSuppressProcessError("uncaughtException", [error])) return;
|
|
70
|
+
uninstallCaptureCallbackIfIdle(true);
|
|
71
|
+
if (originalProcessEmit?.("uncaughtException", error)) return;
|
|
72
|
+
throw error;
|
|
73
|
+
});
|
|
74
|
+
captureCallbackInstalled = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function uninstallCaptureCallbackIfIdle(force = false): void {
|
|
78
|
+
if (!captureCallbackInstalled) return;
|
|
79
|
+
if (!force && activeSuppressions.size > 0) return;
|
|
80
|
+
process.setUncaughtExceptionCaptureCallback(null);
|
|
81
|
+
captureCallbackInstalled = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function uninstallProcessEmitPatchIfIdle(): void {
|
|
85
|
+
if (activeSuppressions.size > 0 || !originalProcessEmit) return;
|
|
86
|
+
uninstallCaptureCallbackIfIdle();
|
|
87
|
+
process.emit = originalProcessEmit as typeof process.emit;
|
|
88
|
+
originalProcessEmit = undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function installCursorSdkAbortErrorSuppression(): CursorSdkAbortErrorSuppression {
|
|
92
|
+
installProcessEmitPatch();
|
|
93
|
+
const token: CursorSdkAbortErrorSuppressionToken = { suppress: false };
|
|
94
|
+
activeSuppressions.add(token);
|
|
95
|
+
let disposed = false;
|
|
96
|
+
return {
|
|
97
|
+
suppressAbortErrors(): void {
|
|
98
|
+
if (disposed) return;
|
|
99
|
+
token.suppress = true;
|
|
100
|
+
installCaptureCallbackIfAvailable();
|
|
101
|
+
},
|
|
102
|
+
dispose(): void {
|
|
103
|
+
if (disposed) return;
|
|
104
|
+
disposed = true;
|
|
105
|
+
activeSuppressions.delete(token);
|
|
106
|
+
uninstallProcessEmitPatchIfIdle();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|