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