pi-cursor-sdk 0.1.17 → 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 +62 -0
- package/README.md +38 -1
- package/docs/cursor-live-smoke-checklist.md +22 -2
- package/docs/cursor-model-ux-spec.md +5 -4
- package/docs/cursor-native-tool-replay.md +96 -2
- package/docs/cursor-testing-lessons.md +428 -0
- package/package.json +11 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/isolated-cursor-smoke.sh +226 -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 +86 -7
- 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-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -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-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +14 -7
- 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 +208 -63
- package/src/cursor-provider-turn-coordinator.ts +217 -47
- package/src/cursor-provider.ts +275 -83
- package/src/cursor-question-tool.ts +10 -5
- 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 {
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
getCursorPromptOptions,
|
|
29
30
|
} from "./cursor-usage-accounting.js";
|
|
30
31
|
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
32
|
+
import { getActiveContextToolNames } from "./cursor-context-tools.js";
|
|
31
33
|
import { CursorLiveRunAbortError, type CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
32
34
|
import {
|
|
33
35
|
abandonSessionCursorAgent,
|
|
@@ -35,9 +37,11 @@ import {
|
|
|
35
37
|
cursorLiveRuns,
|
|
36
38
|
drainCursorLiveRunTurn,
|
|
37
39
|
drainExistingCursorLiveRunBeforeSend,
|
|
40
|
+
flushPendingCursorLiveRunTraceEventsToStream,
|
|
38
41
|
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
39
42
|
getPendingCursorLiveRun,
|
|
40
43
|
hasTrailingUserMessagesAfterToolResults,
|
|
44
|
+
releaseAllPendingCursorLiveRunsForTests,
|
|
41
45
|
resetCursorNativeReplayIdleDisposeMs,
|
|
42
46
|
selectCursorFinalText,
|
|
43
47
|
setCursorNativeReplayIdleDisposeMs,
|
|
@@ -46,8 +50,27 @@ import {
|
|
|
46
50
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
47
51
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
48
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";
|
|
49
58
|
import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
50
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";
|
|
51
74
|
|
|
52
75
|
function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
53
76
|
return {
|
|
@@ -70,25 +93,6 @@ function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
73
|
-
const MISSING_API_KEY_MESSAGE =
|
|
74
|
-
"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.";
|
|
75
|
-
const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
76
|
-
"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.";
|
|
77
|
-
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
78
|
-
"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.";
|
|
79
|
-
const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
80
|
-
|
|
81
|
-
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
82
|
-
import { hasUsableText } from "./cursor-record-utils.js";
|
|
83
|
-
|
|
84
|
-
function isGenericErrorMessage(message: string): boolean {
|
|
85
|
-
const normalized = message.trim().toLowerCase();
|
|
86
|
-
return normalized === "" || normalized === "error" || normalized === "unknown error";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isLikelyAuthError(message: string): boolean {
|
|
90
|
-
return /\b(unauthorized|unauthorised|forbidden|invalid api key|invalid key|authentication|auth|401|403)\b/i.test(message);
|
|
91
|
-
}
|
|
92
96
|
|
|
93
97
|
function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
94
98
|
const trimmed = apiKey?.trim();
|
|
@@ -97,27 +101,6 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
|
97
101
|
return trimmed;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
function resolveCursorSettingSources(): SettingSource[] | undefined {
|
|
101
|
-
const raw = process.env[CURSOR_SETTING_SOURCES_ENV]?.trim();
|
|
102
|
-
if (!raw) return ["all"];
|
|
103
|
-
const normalized = raw.toLowerCase();
|
|
104
|
-
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
105
|
-
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
106
|
-
return raw
|
|
107
|
-
.split(",")
|
|
108
|
-
.map((entry) => entry.trim())
|
|
109
|
-
.filter((entry): entry is SettingSource => Boolean(entry));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function sanitizeError(error: unknown, apiKey?: string): string {
|
|
113
|
-
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
114
|
-
if (message === MISSING_API_KEY_MESSAGE) return MISSING_API_KEY_MESSAGE;
|
|
115
|
-
const scrubbed = scrubSensitiveText(message, apiKey).trim();
|
|
116
|
-
if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
117
|
-
if (isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
|
|
118
|
-
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
104
|
async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<void> {
|
|
122
105
|
try {
|
|
123
106
|
const platform = await createAgentPlatform();
|
|
@@ -135,6 +118,8 @@ export function streamCursor(
|
|
|
135
118
|
options?: SimpleStreamOptions,
|
|
136
119
|
): AssistantMessageEventStream {
|
|
137
120
|
const stream = createAssistantMessageEventStream();
|
|
121
|
+
const sdkEventDebugRef: { current?: CursorSdkEventDebugSink } = {};
|
|
122
|
+
attachCursorSdkEventDebugPiStreamTap(stream, sdkEventDebugRef);
|
|
138
123
|
|
|
139
124
|
(async () => {
|
|
140
125
|
const partial = makeInitialMessage(model);
|
|
@@ -148,8 +133,51 @@ export function streamCursor(
|
|
|
148
133
|
let abortSignal: AbortSignal | undefined;
|
|
149
134
|
let abortListener: (() => void) | undefined;
|
|
150
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
|
+
};
|
|
151
177
|
|
|
152
178
|
try {
|
|
179
|
+
let turnCoordinatorForCleanup: CursorSdkTurnCoordinator | undefined;
|
|
180
|
+
try {
|
|
153
181
|
const throwIfAborted = (): void => {
|
|
154
182
|
if (options?.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
155
183
|
};
|
|
@@ -157,21 +185,29 @@ export function streamCursor(
|
|
|
157
185
|
stream.push({ type: "start", partial });
|
|
158
186
|
throwIfAborted();
|
|
159
187
|
|
|
160
|
-
|
|
161
|
-
|
|
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;
|
|
162
201
|
return;
|
|
163
202
|
}
|
|
164
203
|
|
|
165
204
|
const apiKey = resolveCursorApiKey(options?.apiKey);
|
|
166
|
-
if (!apiKey) throw new Error(
|
|
205
|
+
if (!apiKey) throw new Error(MISSING_CURSOR_API_KEY_MESSAGE);
|
|
167
206
|
resolvedApiKey = apiKey;
|
|
168
207
|
|
|
169
|
-
// pi-ai Context/SimpleStreamOptions do not expose ExtensionContext.cwd; bridge via session_start
|
|
170
|
-
// until pi threads session cwd into streamSimple (cwd can change without a new session event).
|
|
171
|
-
const cwd = getCursorSessionCwd();
|
|
172
208
|
const fastEnabled = getEffectiveFastForModelId(model.id);
|
|
173
209
|
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
174
|
-
const settingSources =
|
|
210
|
+
const settingSources = getEffectiveCursorSettingSources();
|
|
175
211
|
|
|
176
212
|
installCursorMcpToolTimeoutOverride();
|
|
177
213
|
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
@@ -180,6 +216,7 @@ export function streamCursor(
|
|
|
180
216
|
cwd,
|
|
181
217
|
modelSelection: selection,
|
|
182
218
|
settingSources,
|
|
219
|
+
debugRecorder: sdkEventDebug,
|
|
183
220
|
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
184
221
|
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
185
222
|
cursorLiveRuns.queueEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
@@ -197,18 +234,39 @@ export function streamCursor(
|
|
|
197
234
|
throwIfAborted();
|
|
198
235
|
|
|
199
236
|
const promptOptions = getCursorPromptOptions(model);
|
|
200
|
-
let
|
|
201
|
-
|
|
237
|
+
let sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
|
|
238
|
+
let prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
|
|
239
|
+
if (sendPlan.resetAgent) {
|
|
202
240
|
await resetSessionCursorAgent(sessionAgentLease.scopeKey);
|
|
203
241
|
sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
204
242
|
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
205
243
|
agent = sessionAgentLease.agent;
|
|
206
244
|
bridgeRun = sessionAgentLease.bridgeRun;
|
|
207
|
-
|
|
245
|
+
sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
|
|
246
|
+
prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
|
|
208
247
|
}
|
|
248
|
+
const bootstrap = sendPlan.mode === "bootstrap";
|
|
209
249
|
const sessionBridgeRun = sessionAgentLease.bridgeRun;
|
|
210
250
|
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
211
251
|
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
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
|
+
});
|
|
212
270
|
const nativeReplayId = createCursorNativeReplayId();
|
|
213
271
|
const textDeltas: string[] = [];
|
|
214
272
|
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
@@ -221,6 +279,7 @@ export function streamCursor(
|
|
|
221
279
|
sessionAgentScopeKey,
|
|
222
280
|
promptInputTokens,
|
|
223
281
|
textDeltas,
|
|
282
|
+
debugRecorder: sdkEventDebug,
|
|
224
283
|
})
|
|
225
284
|
: undefined;
|
|
226
285
|
if (liveRun) {
|
|
@@ -237,13 +296,17 @@ export function streamCursor(
|
|
|
237
296
|
resolvedApiKey,
|
|
238
297
|
liveRun,
|
|
239
298
|
useNativeToolReplay,
|
|
299
|
+
activeToolNames,
|
|
240
300
|
nativeReplayId,
|
|
241
301
|
textDeltas,
|
|
302
|
+
debugRecorder: sdkEventDebug,
|
|
242
303
|
});
|
|
304
|
+
turnCoordinatorForCleanup = turnCoordinator;
|
|
243
305
|
|
|
244
306
|
// Handle abort signal
|
|
245
307
|
let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
|
|
246
308
|
abortListener = () => {
|
|
309
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
247
310
|
activeLiveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
248
311
|
if (run) {
|
|
249
312
|
run.cancel().catch(() => {});
|
|
@@ -253,25 +316,73 @@ export function streamCursor(
|
|
|
253
316
|
abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
254
317
|
|
|
255
318
|
throwIfAborted();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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);
|
|
261
342
|
},
|
|
262
|
-
|
|
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
|
+
});
|
|
263
359
|
if (liveRun) cursorLiveRuns.attachSdkRun(liveRun, run);
|
|
264
360
|
if (options?.signal?.aborted) {
|
|
361
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
362
|
+
liveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
265
363
|
await run.cancel().catch(() => {});
|
|
266
364
|
throw new CursorLiveRunAbortError();
|
|
267
365
|
}
|
|
366
|
+
const activeRun = run;
|
|
268
367
|
|
|
269
368
|
if (liveRun) {
|
|
270
|
-
|
|
369
|
+
deferSdkEventDebugFinalize = true;
|
|
370
|
+
const waitCompletion = run
|
|
271
371
|
.wait()
|
|
272
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);
|
|
273
385
|
if (liveRun.disposed) return;
|
|
274
|
-
turnCoordinator.discardIncompleteStartedToolCalls();
|
|
275
386
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
276
387
|
if (liveRun.disposed) return;
|
|
277
388
|
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
@@ -281,14 +392,30 @@ export function streamCursor(
|
|
|
281
392
|
selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, turnCoordinator.planTextCandidate),
|
|
282
393
|
);
|
|
283
394
|
} else if (result.status === "cancelled" || options?.signal?.aborted) {
|
|
284
|
-
cursorLiveRuns.markCancelled(
|
|
395
|
+
cursorLiveRuns.markCancelled(
|
|
396
|
+
liveRun,
|
|
397
|
+
formatCursorSdkAbortMessage(
|
|
398
|
+
resolveCursorSdkAbortCause({
|
|
399
|
+
signalAborted: options?.signal?.aborted,
|
|
400
|
+
sdkStatusCancelled: result.status === "cancelled",
|
|
401
|
+
}),
|
|
402
|
+
),
|
|
403
|
+
);
|
|
285
404
|
} else {
|
|
286
|
-
|
|
405
|
+
const failureDetail = formatCursorSdkRunFailureDetail(result, run?.result);
|
|
406
|
+
cursorLiveRuns.markError(
|
|
407
|
+
liveRun,
|
|
408
|
+
sanitizeCursorProviderError(failureDetail, resolvedApiKey ?? options?.apiKey),
|
|
409
|
+
);
|
|
287
410
|
}
|
|
288
411
|
})
|
|
289
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);
|
|
290
417
|
if (liveRun.disposed) return;
|
|
291
|
-
cursorLiveRuns.markError(liveRun,
|
|
418
|
+
cursorLiveRuns.markError(liveRun, sanitizeCursorProviderError(error, resolvedApiKey ?? options?.apiKey));
|
|
292
419
|
});
|
|
293
420
|
|
|
294
421
|
try {
|
|
@@ -296,18 +423,53 @@ export function streamCursor(
|
|
|
296
423
|
await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
|
|
297
424
|
await settleCursorLiveToolBatch(liveRun);
|
|
298
425
|
turnCoordinator.closeTraceBlock();
|
|
299
|
-
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
|
+
});
|
|
300
431
|
});
|
|
301
432
|
} catch (error) {
|
|
302
|
-
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
|
+
}
|
|
303
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(() => {});
|
|
304
455
|
}
|
|
305
456
|
agent = null;
|
|
306
457
|
return;
|
|
307
458
|
}
|
|
308
459
|
|
|
309
460
|
const result = await run.wait();
|
|
310
|
-
|
|
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);
|
|
311
473
|
await cacheSdkContextWindow(agent.agentId, model.id);
|
|
312
474
|
|
|
313
475
|
// Close any open thinking/activity trace, then use the final run result only when
|
|
@@ -317,11 +479,18 @@ export function streamCursor(
|
|
|
317
479
|
if (result.status === "cancelled") {
|
|
318
480
|
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
319
481
|
partial.stopReason = "aborted";
|
|
482
|
+
partial.errorMessage = formatCursorSdkAbortMessage(
|
|
483
|
+
resolveCursorSdkAbortCause({
|
|
484
|
+
signalAborted: options?.signal?.aborted,
|
|
485
|
+
sdkStatusCancelled: true,
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
320
488
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
321
489
|
} else if (result.status === "error") {
|
|
322
490
|
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
323
491
|
partial.stopReason = "error";
|
|
324
|
-
|
|
492
|
+
const failureDetail = formatCursorSdkRunFailureDetail(result, run.result);
|
|
493
|
+
partial.errorMessage = sanitizeCursorProviderError(failureDetail, resolvedApiKey ?? options?.apiKey);
|
|
325
494
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
326
495
|
} else {
|
|
327
496
|
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
@@ -332,27 +501,49 @@ export function streamCursor(
|
|
|
332
501
|
applyCursorApproximateUsage(partial, model, context, promptInputTokens);
|
|
333
502
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
334
503
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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?.();
|
|
348
528
|
|
|
349
|
-
|
|
350
|
-
|
|
529
|
+
if (abortSignal && abortListener) {
|
|
530
|
+
abortSignal.removeEventListener("abort", abortListener);
|
|
531
|
+
}
|
|
351
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");
|
|
352
537
|
}
|
|
353
538
|
|
|
354
539
|
stream.end();
|
|
355
|
-
})()
|
|
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
|
+
});
|
|
356
547
|
|
|
357
548
|
return stream;
|
|
358
549
|
}
|
|
@@ -365,5 +556,6 @@ export const __testUtils = {
|
|
|
365
556
|
hasTrailingUserMessagesAfterToolResults,
|
|
366
557
|
setCursorNativeReplayIdleDisposeMs,
|
|
367
558
|
resetCursorNativeReplayIdleDisposeMs,
|
|
559
|
+
releaseAllPendingCursorLiveRunsForTests,
|
|
368
560
|
resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
|
|
369
561
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
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";
|
|
@@ -36,6 +37,8 @@ interface CursorQuestionDetails {
|
|
|
36
37
|
|
|
37
38
|
interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> {
|
|
38
39
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
40
|
+
on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
|
|
41
|
+
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
39
42
|
on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -81,10 +84,6 @@ const CursorAskQuestionParamsSchema = Type.Object({
|
|
|
81
84
|
questions: Type.Optional(Type.Array(QuestionSchema, { description: "Ask multiple questions sequentially" })),
|
|
82
85
|
});
|
|
83
86
|
|
|
84
|
-
function isCursorModel(model: ExtensionContext["model"]): boolean {
|
|
85
|
-
return model?.provider === "cursor" || model?.api === "cursor-sdk";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
87
|
function normalizeOption(option: RawQuestionOption, index: number): CursorQuestionOption | undefined {
|
|
89
88
|
if (typeof option === "string") {
|
|
90
89
|
const trimmed = option.trim();
|
|
@@ -246,6 +245,12 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
|
|
|
246
245
|
pi.on("session_start", (_event, ctx) => {
|
|
247
246
|
syncCursorQuestionToolForModel(pi, ctx.model);
|
|
248
247
|
});
|
|
248
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
249
|
+
syncCursorQuestionToolForModel(pi, ctx.model);
|
|
250
|
+
});
|
|
251
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
252
|
+
syncCursorQuestionToolForModel(pi, ctx.model);
|
|
253
|
+
});
|
|
249
254
|
pi.on("model_select", (event) => {
|
|
250
255
|
syncCursorQuestionToolForModel(pi, event.model);
|
|
251
256
|
});
|