pi-cursor-sdk 0.1.14 → 0.1.16
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 +57 -0
- package/README.md +68 -14
- package/docs/cursor-live-smoke-checklist.md +271 -0
- package/docs/cursor-model-ux-spec.md +27 -4
- package/docs/cursor-native-tool-replay.md +99 -0
- package/docs/cursor-native-tool-visual-audit.md +183 -0
- package/package.json +6 -2
- package/src/context.ts +214 -16
- package/src/cursor-bridge-contract.ts +27 -0
- package/src/cursor-live-run-accounting.ts +65 -0
- package/src/cursor-mcp-timeout-override.ts +111 -0
- package/src/cursor-native-tool-display.ts +409 -49
- package/src/cursor-pi-tool-bridge.ts +1174 -0
- package/src/cursor-provider.ts +614 -146
- package/src/cursor-question-tool.ts +252 -0
- package/src/cursor-session-agent.ts +372 -0
- package/src/cursor-session-cwd.ts +28 -0
- package/src/cursor-session-scope.ts +65 -0
- package/src/cursor-state.ts +38 -10
- package/src/cursor-tool-names.ts +67 -0
- package/src/cursor-tool-transcript.ts +730 -61
- package/src/cursor-usage-accounting.ts +71 -0
- package/src/index.ts +27 -3
package/src/cursor-provider.ts
CHANGED
|
@@ -6,14 +6,39 @@ import {
|
|
|
6
6
|
type Model,
|
|
7
7
|
type SimpleStreamOptions,
|
|
8
8
|
type AssistantMessage,
|
|
9
|
+
type ToolResultMessage,
|
|
9
10
|
} from "@earendil-works/pi-ai";
|
|
11
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
12
|
import { Agent, createAgentPlatform } from "@cursor/sdk";
|
|
11
13
|
import type { InteractionUpdate, SDKAgent, SettingSource } from "@cursor/sdk";
|
|
12
|
-
import {
|
|
14
|
+
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
15
|
+
import { buildCursorSendPrompt } from "./context.js";
|
|
16
|
+
import {
|
|
17
|
+
acquireSessionCursorAgent,
|
|
18
|
+
commitSessionAgentSend,
|
|
19
|
+
disposeAllSessionCursorAgents,
|
|
20
|
+
resetSessionCursorAgent,
|
|
21
|
+
} from "./cursor-session-agent.js";
|
|
22
|
+
import {
|
|
23
|
+
type CursorPiBridgeToolRequest,
|
|
24
|
+
type CursorPiToolBridgeRun,
|
|
25
|
+
} from "./cursor-pi-tool-bridge.js";
|
|
26
|
+
import {
|
|
27
|
+
consumeCursorLiveToolResults,
|
|
28
|
+
createCursorLiveRunAccountingState,
|
|
29
|
+
takeCursorLiveTurnInputTokens,
|
|
30
|
+
type CursorLiveRunAccountingState,
|
|
31
|
+
} from "./cursor-live-run-accounting.js";
|
|
32
|
+
import {
|
|
33
|
+
applyCursorApproximateUsage,
|
|
34
|
+
estimateCursorPromptInputTokens,
|
|
35
|
+
getCursorPromptOptions,
|
|
36
|
+
} from "./cursor-usage-accounting.js";
|
|
37
|
+
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
13
38
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
14
39
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
15
40
|
import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
|
|
16
|
-
import { buildCursorPiToolDisplay, formatCursorToolTranscript, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
|
|
41
|
+
import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
|
|
17
42
|
import {
|
|
18
43
|
canRenderCursorToolNatively,
|
|
19
44
|
isCursorNativeToolDisplayRuntimeEnabled,
|
|
@@ -56,26 +81,34 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
56
81
|
"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.";
|
|
57
82
|
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
58
83
|
"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.";
|
|
59
|
-
const APPROX_CHARS_PER_TOKEN = 4;
|
|
60
|
-
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
61
84
|
const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
|
|
62
85
|
const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
63
86
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
64
87
|
const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
88
|
+
const cursorSdkOutputSuppression = new AsyncLocalStorage<boolean>();
|
|
65
89
|
|
|
66
|
-
type
|
|
90
|
+
type CursorLiveQueuedEvent =
|
|
67
91
|
| { type: "thinking-delta"; text: string }
|
|
68
92
|
| { type: "thinking-completed" }
|
|
69
93
|
| { type: "text-delta"; text: string }
|
|
70
|
-
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
94
|
+
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
95
|
+
| { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
|
|
96
|
+
|
|
97
|
+
interface CursorLiveSdkRun {
|
|
98
|
+
cancel(): Promise<void>;
|
|
99
|
+
}
|
|
71
100
|
|
|
72
|
-
interface
|
|
101
|
+
interface CursorLiveRun {
|
|
73
102
|
id: string;
|
|
74
103
|
agent: SDKAgent;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
bridgeRun?: CursorPiToolBridgeRun;
|
|
105
|
+
sessionBridgeRun?: CursorPiToolBridgeRun;
|
|
106
|
+
sessionAgentScopeKey?: string;
|
|
107
|
+
sdkRun?: CursorLiveSdkRun;
|
|
108
|
+
accounting: CursorLiveRunAccountingState;
|
|
109
|
+
pendingEvents: CursorLiveQueuedEvent[];
|
|
78
110
|
textDeltas: string[];
|
|
111
|
+
emittedText: string;
|
|
79
112
|
recordedToolDisplayIds: string[];
|
|
80
113
|
finalText?: string;
|
|
81
114
|
done: boolean;
|
|
@@ -86,16 +119,17 @@ interface CursorNativeLiveRun {
|
|
|
86
119
|
waiters: Set<() => void>;
|
|
87
120
|
}
|
|
88
121
|
|
|
89
|
-
interface
|
|
122
|
+
interface CursorLiveTurnState {
|
|
90
123
|
stream: AssistantMessageEventStream;
|
|
91
124
|
partial: AssistantMessage;
|
|
92
125
|
thinkingContentIndex: number;
|
|
93
126
|
textContentIndex: number;
|
|
127
|
+
emittedText: string;
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
let cursorNativeReplayCounter = 0;
|
|
97
131
|
let cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
98
|
-
const
|
|
132
|
+
const pendingCursorLiveRuns = new Map<string, CursorLiveRun>();
|
|
99
133
|
|
|
100
134
|
function escapeRegExp(value: string): string {
|
|
101
135
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -134,7 +168,7 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
|
134
168
|
|
|
135
169
|
function resolveCursorSettingSources(): SettingSource[] | undefined {
|
|
136
170
|
const raw = process.env[CURSOR_SETTING_SOURCES_ENV]?.trim();
|
|
137
|
-
if (!raw) return
|
|
171
|
+
if (!raw) return ["all"];
|
|
138
172
|
const normalized = raw.toLowerCase();
|
|
139
173
|
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
140
174
|
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
@@ -153,6 +187,78 @@ function sanitizeError(error: unknown, apiKey?: string): string {
|
|
|
153
187
|
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
154
188
|
}
|
|
155
189
|
|
|
190
|
+
function isCursorSdkOutputSuppressed(): boolean {
|
|
191
|
+
return cursorSdkOutputSuppression.getStore() === true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function suppressCursorSdkOutput<T>(operation: () => Promise<T>): Promise<T> {
|
|
195
|
+
return cursorSdkOutputSuppression.run(true, operation);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const CURSOR_SDK_STARTUP_NOISE_PATTERNS = [
|
|
199
|
+
"managed_skills.",
|
|
200
|
+
"CursorPluginsAgentSkillsService load completed",
|
|
201
|
+
"LocalCursorRulesService load completed",
|
|
202
|
+
"AgentSkillsCursorRulesService load completed",
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
function isCursorSdkStartupNoise(text: string): boolean {
|
|
206
|
+
return CURSOR_SDK_STARTUP_NOISE_PATTERNS.some((pattern) => text.includes(pattern));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function createFilteredProcessWrite<TWrite extends typeof process.stdout.write>(write: TWrite, stream: NodeJS.WriteStream): TWrite {
|
|
210
|
+
return ((
|
|
211
|
+
chunk: string | Uint8Array,
|
|
212
|
+
encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
|
|
213
|
+
callback?: (error?: Error | null) => void,
|
|
214
|
+
): boolean => {
|
|
215
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
216
|
+
if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) {
|
|
217
|
+
const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
218
|
+
done?.();
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return write.call(stream, chunk as string, encodingOrCallback as BufferEncoding, callback);
|
|
222
|
+
}) as TWrite;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function createFilteredConsoleMethod<TMethod extends typeof console.log>(method: TMethod): TMethod {
|
|
226
|
+
return ((...args: Parameters<TMethod>): void => {
|
|
227
|
+
const text = args.map((arg) => (typeof arg === "string" ? arg : String(arg))).join(" ");
|
|
228
|
+
if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) return;
|
|
229
|
+
method(...args);
|
|
230
|
+
}) as TMethod;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function installCursorSdkOutputFilter(): () => void {
|
|
234
|
+
const stdoutWrite = process.stdout.write;
|
|
235
|
+
const stderrWrite = process.stderr.write;
|
|
236
|
+
const consoleLog = console.log;
|
|
237
|
+
const consoleInfo = console.info;
|
|
238
|
+
const consoleWarn = console.warn;
|
|
239
|
+
const consoleError = console.error;
|
|
240
|
+
const consoleDebug = console.debug;
|
|
241
|
+
process.stdout.write = createFilteredProcessWrite(stdoutWrite, process.stdout);
|
|
242
|
+
process.stderr.write = createFilteredProcessWrite(stderrWrite, process.stderr) as typeof process.stderr.write;
|
|
243
|
+
console.log = createFilteredConsoleMethod(consoleLog);
|
|
244
|
+
console.info = createFilteredConsoleMethod(consoleInfo);
|
|
245
|
+
console.warn = createFilteredConsoleMethod(consoleWarn);
|
|
246
|
+
console.error = createFilteredConsoleMethod(consoleError);
|
|
247
|
+
console.debug = createFilteredConsoleMethod(consoleDebug);
|
|
248
|
+
let restored = false;
|
|
249
|
+
return () => {
|
|
250
|
+
if (restored) return;
|
|
251
|
+
restored = true;
|
|
252
|
+
process.stdout.write = stdoutWrite;
|
|
253
|
+
process.stderr.write = stderrWrite;
|
|
254
|
+
console.log = consoleLog;
|
|
255
|
+
console.info = consoleInfo;
|
|
256
|
+
console.warn = consoleWarn;
|
|
257
|
+
console.error = consoleError;
|
|
258
|
+
console.debug = consoleDebug;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
156
262
|
function getObjectField(value: unknown, field: string): unknown {
|
|
157
263
|
if (!value || typeof value !== "object") return undefined;
|
|
158
264
|
return (value as Record<string, unknown>)[field];
|
|
@@ -163,6 +269,7 @@ function getCursorToolName(toolCall: unknown): string {
|
|
|
163
269
|
const data = toolCall as Record<string, unknown>;
|
|
164
270
|
if (typeof data.name === "string") return data.name;
|
|
165
271
|
if (typeof data.type === "string") return data.type;
|
|
272
|
+
if (typeof data.toolName === "string") return data.toolName;
|
|
166
273
|
return "unknown";
|
|
167
274
|
}
|
|
168
275
|
|
|
@@ -177,27 +284,6 @@ async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<
|
|
|
177
284
|
}
|
|
178
285
|
}
|
|
179
286
|
|
|
180
|
-
function estimateTextTokens(text: string): number {
|
|
181
|
-
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function estimatePromptInputTokens(prompt: CursorPrompt): number {
|
|
185
|
-
return estimateTextTokens(prompt.text) + prompt.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function getPromptInputTokenBudget(model: Model<Api>): number {
|
|
189
|
-
const outputReserveTokens = Math.min(model.maxTokens, Math.max(1, Math.floor(model.contextWindow * 0.2)));
|
|
190
|
-
return Math.max(1, model.contextWindow - outputReserveTokens);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function setApproximateUsage(partial: AssistantMessage, promptInputTokens: number, outputText: string): void {
|
|
194
|
-
partial.usage.input = promptInputTokens;
|
|
195
|
-
partial.usage.output = estimateTextTokens(outputText);
|
|
196
|
-
partial.usage.cacheRead = 0;
|
|
197
|
-
partial.usage.cacheWrite = 0;
|
|
198
|
-
partial.usage.totalTokens = partial.usage.input + partial.usage.output;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
287
|
function sanitizeSingleLine(value: string): string {
|
|
202
288
|
return value.replace(/\s+/g, " ").trim();
|
|
203
289
|
}
|
|
@@ -215,6 +301,63 @@ function hasUsableText(value: string | undefined): value is string {
|
|
|
215
301
|
return typeof value === "string" && value.trim().length > 0;
|
|
216
302
|
}
|
|
217
303
|
|
|
304
|
+
interface CursorShellOutputDelta {
|
|
305
|
+
stream: "stdout" | "stderr";
|
|
306
|
+
data: string;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
interface CursorShellOutputDeltas {
|
|
310
|
+
stdout: string[];
|
|
311
|
+
stderr: string[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isCursorShellToolCall(toolCall: unknown): boolean {
|
|
315
|
+
const normalizedName = getCursorToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase();
|
|
316
|
+
return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
|
|
320
|
+
if (update.type !== "shell-output-delta") return undefined;
|
|
321
|
+
const event = getObjectField(update, "event");
|
|
322
|
+
const eventCase = getObjectField(event, "case");
|
|
323
|
+
if (eventCase !== "stdout" && eventCase !== "stderr") return undefined;
|
|
324
|
+
const value = getObjectField(event, "value");
|
|
325
|
+
const data = getObjectField(value, "data");
|
|
326
|
+
if (typeof data !== "string" || data.length === 0) return undefined;
|
|
327
|
+
return { stream: eventCase, data };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: CursorShellOutputDeltas | undefined): unknown {
|
|
331
|
+
if (!deltas) return toolCall;
|
|
332
|
+
const stdout = deltas.stdout.join("");
|
|
333
|
+
const stderr = deltas.stderr.join("");
|
|
334
|
+
if (!hasUsableText(stdout) && !hasUsableText(stderr)) return toolCall;
|
|
335
|
+
|
|
336
|
+
const toolRecord = toolCall && typeof toolCall === "object" && !Array.isArray(toolCall) ? (toolCall as Record<string, unknown>) : undefined;
|
|
337
|
+
const result = getObjectField(toolRecord, "result");
|
|
338
|
+
const resultRecord = result && typeof result === "object" && !Array.isArray(result) ? (result as Record<string, unknown>) : undefined;
|
|
339
|
+
if (!toolRecord || !resultRecord || resultRecord.status !== "success") return toolCall;
|
|
340
|
+
|
|
341
|
+
const value = getObjectField(resultRecord, "value");
|
|
342
|
+
const valueRecord = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
343
|
+
const completedStdout = getObjectField(valueRecord, "stdout");
|
|
344
|
+
const completedStderr = getObjectField(valueRecord, "stderr");
|
|
345
|
+
if (hasUsableText(typeof completedStdout === "string" ? completedStdout : undefined)) return toolCall;
|
|
346
|
+
if (hasUsableText(typeof completedStderr === "string" ? completedStderr : undefined)) return toolCall;
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
...toolRecord,
|
|
350
|
+
result: {
|
|
351
|
+
...resultRecord,
|
|
352
|
+
value: {
|
|
353
|
+
...(valueRecord ?? {}),
|
|
354
|
+
stdout,
|
|
355
|
+
stderr,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
218
361
|
function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
|
|
219
362
|
if (typeof value === "string") return scrubSensitiveText(value, apiKey);
|
|
220
363
|
if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
|
|
@@ -233,16 +376,34 @@ function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | und
|
|
|
233
376
|
return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
|
|
234
377
|
}
|
|
235
378
|
|
|
236
|
-
function
|
|
379
|
+
function getPendingCursorLiveRun(context: Context): CursorLiveRun | undefined {
|
|
237
380
|
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
|
238
381
|
const message = context.messages[index];
|
|
239
382
|
if (message.role !== "toolResult") break;
|
|
240
383
|
const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
|
|
241
|
-
if (replayId
|
|
384
|
+
if (replayId) {
|
|
385
|
+
const replayRun = pendingCursorLiveRuns.get(replayId);
|
|
386
|
+
if (replayRun) return replayRun;
|
|
387
|
+
}
|
|
388
|
+
for (const run of pendingCursorLiveRuns.values()) {
|
|
389
|
+
if (run.bridgeRun?.hasPendingPiToolCallId(message.toolCallId)) return run;
|
|
390
|
+
}
|
|
242
391
|
}
|
|
243
392
|
return undefined;
|
|
244
393
|
}
|
|
245
394
|
|
|
395
|
+
function isCursorLiveRunToolResult(run: CursorLiveRun, message: ToolResultMessage): boolean {
|
|
396
|
+
const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
|
|
397
|
+
if (replayId) return replayId === run.id;
|
|
398
|
+
return run.bridgeRun?.hasPendingPiToolCallId(message.toolCallId) ?? false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function consumeCursorLiveRunToolResults(run: CursorLiveRun, context: Context) {
|
|
402
|
+
const consumed = consumeCursorLiveToolResults(run.accounting, context, (toolResult) => isCursorLiveRunToolResult(run, toolResult));
|
|
403
|
+
run.accounting = consumed.state;
|
|
404
|
+
return consumed;
|
|
405
|
+
}
|
|
406
|
+
|
|
246
407
|
function splitTextIntoReplayDeltas(text: string): string[] {
|
|
247
408
|
const deltas: string[] = [];
|
|
248
409
|
let remaining = text;
|
|
@@ -278,36 +439,37 @@ async function emitTextDeltas(
|
|
|
278
439
|
return block.text;
|
|
279
440
|
}
|
|
280
441
|
|
|
281
|
-
function notifyCursorNativeRun(run:
|
|
442
|
+
function notifyCursorNativeRun(run: CursorLiveRun): void {
|
|
282
443
|
for (const waiter of run.waiters) waiter();
|
|
283
444
|
run.waiters.clear();
|
|
284
445
|
}
|
|
285
446
|
|
|
286
|
-
function queueCursorNativeEvent(run:
|
|
447
|
+
function queueCursorNativeEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void {
|
|
287
448
|
run.pendingEvents.push(event);
|
|
288
449
|
notifyCursorNativeRun(run);
|
|
289
450
|
}
|
|
290
451
|
|
|
291
|
-
function clearCursorNativeRunIdleDispose(run:
|
|
452
|
+
function clearCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
292
453
|
if (!run.idleDisposeTimer) return;
|
|
293
454
|
clearTimeout(run.idleDisposeTimer);
|
|
294
455
|
run.idleDisposeTimer = undefined;
|
|
295
456
|
}
|
|
296
457
|
|
|
297
|
-
function scheduleCursorNativeRunIdleDispose(run:
|
|
458
|
+
function scheduleCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
298
459
|
if (run.disposed) return;
|
|
299
460
|
clearCursorNativeRunIdleDispose(run);
|
|
300
461
|
run.idleDisposeTimer = setTimeout(() => {
|
|
301
|
-
void
|
|
462
|
+
void releaseCursorLiveRun(run);
|
|
302
463
|
}, cursorNativeReplayIdleDisposeMs);
|
|
303
464
|
run.idleDisposeTimer.unref?.();
|
|
304
465
|
}
|
|
305
466
|
|
|
306
|
-
function isCursorNativeRunReady(run:
|
|
467
|
+
function isCursorNativeRunReady(run: CursorLiveRun): boolean {
|
|
307
468
|
return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
|
|
308
469
|
}
|
|
309
470
|
|
|
310
|
-
async function waitForCursorNativeRunProgress(run:
|
|
471
|
+
async function waitForCursorNativeRunProgress(run: CursorLiveRun, signal?: AbortSignal): Promise<void> {
|
|
472
|
+
if (signal?.aborted) throw new CursorAbortError();
|
|
311
473
|
if (isCursorNativeRunReady(run)) return;
|
|
312
474
|
await new Promise<void>((resolve, reject) => {
|
|
313
475
|
let waiter: (() => void) | undefined;
|
|
@@ -324,16 +486,21 @@ async function waitForCursorNativeRunProgress(run: CursorNativeLiveRun, signal?:
|
|
|
324
486
|
resolve();
|
|
325
487
|
};
|
|
326
488
|
run.waiters.add(waiter);
|
|
489
|
+
if (signal?.aborted) {
|
|
490
|
+
onAbort();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
327
493
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
328
494
|
});
|
|
329
495
|
}
|
|
330
496
|
|
|
331
|
-
async function
|
|
332
|
-
|
|
497
|
+
async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<void> {
|
|
498
|
+
const eventType = run.pendingEvents[0]?.type;
|
|
499
|
+
if (eventType !== "tool" && eventType !== "bridge-tool") return;
|
|
333
500
|
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
334
501
|
}
|
|
335
502
|
|
|
336
|
-
function closeCursorNativeThinkingBlock(turn:
|
|
503
|
+
function closeCursorNativeThinkingBlock(turn: CursorLiveTurnState): void {
|
|
337
504
|
if (turn.thinkingContentIndex < 0) return;
|
|
338
505
|
const block = turn.partial.content[turn.thinkingContentIndex];
|
|
339
506
|
if (block.type === "thinking") {
|
|
@@ -347,7 +514,7 @@ function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
|
|
|
347
514
|
turn.thinkingContentIndex = -1;
|
|
348
515
|
}
|
|
349
516
|
|
|
350
|
-
function closeCursorNativeTextBlock(turn:
|
|
517
|
+
function closeCursorNativeTextBlock(turn: CursorLiveTurnState): string {
|
|
351
518
|
if (turn.textContentIndex < 0) return "";
|
|
352
519
|
const contentIndex = turn.textContentIndex;
|
|
353
520
|
const block = turn.partial.content[contentIndex];
|
|
@@ -362,12 +529,12 @@ function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
|
|
|
362
529
|
return block.text;
|
|
363
530
|
}
|
|
364
531
|
|
|
365
|
-
function closeCursorNativeTurnBlocks(turn:
|
|
532
|
+
function closeCursorNativeTurnBlocks(turn: CursorLiveTurnState): string {
|
|
366
533
|
closeCursorNativeThinkingBlock(turn);
|
|
367
534
|
return closeCursorNativeTextBlock(turn);
|
|
368
535
|
}
|
|
369
536
|
|
|
370
|
-
function emitCursorNativeThinkingDelta(turn:
|
|
537
|
+
function emitCursorNativeThinkingDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
371
538
|
closeCursorNativeTextBlock(turn);
|
|
372
539
|
if (turn.thinkingContentIndex < 0) {
|
|
373
540
|
turn.thinkingContentIndex = turn.partial.content.length;
|
|
@@ -380,7 +547,7 @@ function emitCursorNativeThinkingDelta(turn: CursorNativeTurnState, delta: strin
|
|
|
380
547
|
turn.stream.push({ type: "thinking_delta", contentIndex: turn.thinkingContentIndex, delta, partial: turn.partial });
|
|
381
548
|
}
|
|
382
549
|
|
|
383
|
-
function emitCursorNativeTextDelta(turn:
|
|
550
|
+
function emitCursorNativeTextDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
384
551
|
closeCursorNativeThinkingBlock(turn);
|
|
385
552
|
if (turn.textContentIndex < 0) {
|
|
386
553
|
turn.textContentIndex = turn.partial.content.length;
|
|
@@ -393,20 +560,23 @@ function emitCursorNativeTextDelta(turn: CursorNativeTurnState, delta: string):
|
|
|
393
560
|
turn.stream.push({ type: "text_delta", contentIndex: turn.textContentIndex, delta, partial: turn.partial });
|
|
394
561
|
}
|
|
395
562
|
|
|
396
|
-
function
|
|
397
|
-
turn:
|
|
398
|
-
event: Exclude<
|
|
563
|
+
function emitCursorLiveQueuedEvent(
|
|
564
|
+
turn: CursorLiveTurnState,
|
|
565
|
+
event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
|
|
566
|
+
run?: CursorLiveRun,
|
|
399
567
|
): void {
|
|
400
568
|
if (event.type === "thinking-delta") {
|
|
401
569
|
emitCursorNativeThinkingDelta(turn, event.text);
|
|
402
570
|
} else if (event.type === "thinking-completed") {
|
|
403
571
|
closeCursorNativeThinkingBlock(turn);
|
|
404
572
|
} else if (event.type === "text-delta") {
|
|
573
|
+
turn.emittedText += event.text;
|
|
574
|
+
if (run) run.emittedText += event.text;
|
|
405
575
|
emitCursorNativeTextDelta(turn, event.text);
|
|
406
576
|
}
|
|
407
577
|
}
|
|
408
578
|
|
|
409
|
-
function collectCursorNativeToolBatch(run:
|
|
579
|
+
function collectCursorNativeToolBatch(run: CursorLiveRun): CursorNativeToolDisplayItem[] {
|
|
410
580
|
const tools: CursorNativeToolDisplayItem[] = [];
|
|
411
581
|
while (run.pendingEvents[0]?.type === "tool") {
|
|
412
582
|
const event = run.pendingEvents.shift();
|
|
@@ -415,19 +585,81 @@ function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToo
|
|
|
415
585
|
return tools;
|
|
416
586
|
}
|
|
417
587
|
|
|
418
|
-
function
|
|
588
|
+
function collectCursorBridgeToolBatch(run: CursorLiveRun): CursorPiBridgeToolRequest[] {
|
|
589
|
+
const requests: CursorPiBridgeToolRequest[] = [];
|
|
590
|
+
while (run.pendingEvents[0]?.type === "bridge-tool") {
|
|
591
|
+
const event = run.pendingEvents.shift();
|
|
592
|
+
if (event?.type === "bridge-tool") requests.push(event.request);
|
|
593
|
+
}
|
|
594
|
+
return requests;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function isCursorTextBoundary(text: string, index: number): boolean {
|
|
598
|
+
if (index <= 0 || index >= text.length) return true;
|
|
599
|
+
const before = text[index - 1];
|
|
600
|
+
const after = text[index];
|
|
601
|
+
return !/[\p{L}\p{N}_]/u.test(before) || !/[\p{L}\p{N}_]/u.test(after);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function trimAlreadyEmittedCursorText(text: string, emittedText: string, options?: { allowPartialPrefix?: boolean }): string {
|
|
605
|
+
if (!text || !emittedText) return text;
|
|
606
|
+
if (text === emittedText) return "";
|
|
607
|
+
if (text.startsWith(emittedText) && (options?.allowPartialPrefix || isCursorTextBoundary(text, emittedText.length))) {
|
|
608
|
+
return text.slice(emittedText.length);
|
|
609
|
+
}
|
|
610
|
+
if (emittedText.endsWith(text) && isCursorTextBoundary(emittedText, emittedText.length - text.length)) return "";
|
|
611
|
+
const trimmedText = text.trim();
|
|
612
|
+
const trimmedEmittedText = emittedText.trim();
|
|
613
|
+
if (trimmedText === trimmedEmittedText) return "";
|
|
614
|
+
if (trimmedText && trimmedEmittedText.endsWith(trimmedText)) {
|
|
615
|
+
const suffixStart = trimmedEmittedText.length - trimmedText.length;
|
|
616
|
+
if (isCursorTextBoundary(trimmedEmittedText, suffixStart)) return "";
|
|
617
|
+
}
|
|
618
|
+
return text;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function trimCurrentTurnAlreadyEmittedCursorText(text: string, currentTurnEmittedText: string, emittedText = currentTurnEmittedText): string {
|
|
622
|
+
if (!currentTurnEmittedText) return trimAlreadyEmittedCursorText(text, emittedText);
|
|
623
|
+
const currentTurnTrimmedText = trimAlreadyEmittedCursorText(text, currentTurnEmittedText, { allowPartialPrefix: true });
|
|
624
|
+
if (currentTurnTrimmedText !== text) return currentTurnTrimmedText;
|
|
625
|
+
if (emittedText.endsWith(currentTurnEmittedText)) {
|
|
626
|
+
const emittedTextTrimmedText = trimAlreadyEmittedCursorText(text, emittedText, { allowPartialPrefix: true });
|
|
627
|
+
if (emittedTextTrimmedText !== text) return emittedTextTrimmedText;
|
|
628
|
+
}
|
|
629
|
+
return trimAlreadyEmittedCursorText(text, emittedText);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function selectCursorFinalText(
|
|
633
|
+
resultText: unknown,
|
|
634
|
+
textDeltas: readonly string[],
|
|
635
|
+
emittedText: string,
|
|
636
|
+
fallbackText?: string,
|
|
637
|
+
options?: { allowPartialPrefix?: boolean },
|
|
638
|
+
): string {
|
|
639
|
+
const candidates = [typeof resultText === "string" ? resultText : undefined, fallbackText, textDeltas.join("")];
|
|
640
|
+
for (const candidate of candidates) {
|
|
641
|
+
if (!hasUsableText(candidate)) continue;
|
|
642
|
+
const trimmedCandidate = trimAlreadyEmittedCursorText(candidate, emittedText, options);
|
|
643
|
+
if (hasUsableText(trimmedCandidate)) return trimmedCandidate;
|
|
644
|
+
}
|
|
645
|
+
return "";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function takeCursorLiveSessionInputTokens(run: CursorLiveRun, toolResultInputTokens: number): number {
|
|
419
649
|
// Native replay can split one Cursor run into multiple pi turns; count prompt input once.
|
|
420
|
-
|
|
421
|
-
run.
|
|
422
|
-
return
|
|
650
|
+
const taken = takeCursorLiveTurnInputTokens(run.accounting, toolResultInputTokens);
|
|
651
|
+
run.accounting = taken.state;
|
|
652
|
+
return taken.sessionInputTokens;
|
|
423
653
|
}
|
|
424
654
|
|
|
425
655
|
function emitCursorNativeToolUseTurn(
|
|
426
656
|
stream: AssistantMessageEventStream,
|
|
427
657
|
partial: AssistantMessage,
|
|
428
|
-
|
|
658
|
+
model: Model<Api>,
|
|
659
|
+
context: Context,
|
|
660
|
+
run: CursorLiveRun,
|
|
661
|
+
toolResultInputTokens: number,
|
|
429
662
|
tools: CursorNativeToolDisplayItem[],
|
|
430
|
-
outputText: string,
|
|
431
663
|
): void {
|
|
432
664
|
const shouldTerminate = run.done && !run.finalText?.trim() && run.pendingEvents.length === 0;
|
|
433
665
|
for (const tool of tools) {
|
|
@@ -446,76 +678,142 @@ function emitCursorNativeToolUseTurn(
|
|
|
446
678
|
run.recordedToolDisplayIds.push(tool.id);
|
|
447
679
|
}
|
|
448
680
|
}
|
|
449
|
-
|
|
681
|
+
applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
|
|
682
|
+
partial.stopReason = "toolUse";
|
|
683
|
+
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
684
|
+
scheduleCursorNativeRunIdleDispose(run);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function emitCursorBridgeToolUseTurn(
|
|
688
|
+
stream: AssistantMessageEventStream,
|
|
689
|
+
partial: AssistantMessage,
|
|
690
|
+
model: Model<Api>,
|
|
691
|
+
context: Context,
|
|
692
|
+
run: CursorLiveRun,
|
|
693
|
+
toolResultInputTokens: number,
|
|
694
|
+
requests: CursorPiBridgeToolRequest[],
|
|
695
|
+
): void {
|
|
696
|
+
for (const request of requests) {
|
|
697
|
+
const contentIndex = partial.content.length;
|
|
698
|
+
partial.content.push({
|
|
699
|
+
type: "toolCall",
|
|
700
|
+
id: request.piToolCallId,
|
|
701
|
+
name: request.piToolName,
|
|
702
|
+
arguments: request.args,
|
|
703
|
+
});
|
|
704
|
+
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
705
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(request.args), partial });
|
|
706
|
+
const block = partial.content[contentIndex];
|
|
707
|
+
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
708
|
+
}
|
|
709
|
+
applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
|
|
450
710
|
partial.stopReason = "toolUse";
|
|
451
711
|
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
452
712
|
scheduleCursorNativeRunIdleDispose(run);
|
|
453
713
|
}
|
|
454
714
|
|
|
455
|
-
|
|
715
|
+
function isSuccessfulCursorLiveRun(run: CursorLiveRun): boolean {
|
|
716
|
+
return run.done && !run.cancelled && !run.errorMessage;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function abandonSessionCursorAgent(scopeKey: string | undefined): Promise<void> {
|
|
720
|
+
if (!scopeKey) return;
|
|
721
|
+
await resetSessionCursorAgent(scopeKey);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function releaseCursorLiveRun(run: CursorLiveRun): Promise<void> {
|
|
456
725
|
if (run.disposed) return;
|
|
726
|
+
const abandoned = !isSuccessfulCursorLiveRun(run);
|
|
457
727
|
run.disposed = true;
|
|
458
|
-
|
|
728
|
+
pendingCursorLiveRuns.delete(run.id);
|
|
459
729
|
clearCursorNativeRunIdleDispose(run);
|
|
730
|
+
run.bridgeRun?.cancel("Cursor live run released");
|
|
460
731
|
for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
|
|
461
732
|
run.recordedToolDisplayIds = [];
|
|
462
733
|
run.waiters.clear();
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
734
|
+
if (run.sessionBridgeRun) {
|
|
735
|
+
run.sessionBridgeRun.setOnToolRequest(undefined);
|
|
736
|
+
}
|
|
737
|
+
if (run.bridgeRun && run.bridgeRun !== run.sessionBridgeRun) {
|
|
738
|
+
try {
|
|
739
|
+
await run.bridgeRun.dispose();
|
|
740
|
+
} catch {
|
|
741
|
+
// bridge disposal failure should not mask the provider result
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (abandoned) {
|
|
745
|
+
try {
|
|
746
|
+
await run.sdkRun?.cancel();
|
|
747
|
+
} catch {
|
|
748
|
+
// cancellation failure should not block session-agent abandonment
|
|
749
|
+
}
|
|
750
|
+
await abandonSessionCursorAgent(run.sessionAgentScopeKey);
|
|
467
751
|
}
|
|
468
752
|
}
|
|
469
753
|
|
|
470
754
|
async function emitCursorNativeRunNextTurn(
|
|
471
755
|
stream: AssistantMessageEventStream,
|
|
472
756
|
partial: AssistantMessage,
|
|
473
|
-
|
|
757
|
+
model: Model<Api>,
|
|
758
|
+
context: Context,
|
|
759
|
+
run: CursorLiveRun,
|
|
760
|
+
toolResultInputTokens: number,
|
|
474
761
|
signal?: AbortSignal,
|
|
475
762
|
): Promise<void> {
|
|
476
|
-
const turn:
|
|
763
|
+
const turn: CursorLiveTurnState = {
|
|
477
764
|
stream,
|
|
478
765
|
partial,
|
|
479
766
|
thinkingContentIndex: -1,
|
|
480
767
|
textContentIndex: -1,
|
|
768
|
+
emittedText: "",
|
|
481
769
|
};
|
|
482
770
|
|
|
483
771
|
while (true) {
|
|
484
772
|
while (run.pendingEvents.length > 0) {
|
|
485
773
|
const event = run.pendingEvents[0];
|
|
486
774
|
if (event.type === "tool") {
|
|
487
|
-
await
|
|
488
|
-
|
|
775
|
+
await settleCursorLiveToolBatch(run);
|
|
776
|
+
if (signal?.aborted) throw new CursorAbortError();
|
|
777
|
+
closeCursorNativeTurnBlocks(turn);
|
|
489
778
|
const tools = collectCursorNativeToolBatch(run);
|
|
490
|
-
emitCursorNativeToolUseTurn(stream, partial, run,
|
|
779
|
+
emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, tools);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (event.type === "bridge-tool") {
|
|
783
|
+
await settleCursorLiveToolBatch(run);
|
|
784
|
+
if (signal?.aborted) throw new CursorAbortError();
|
|
785
|
+
closeCursorNativeTurnBlocks(turn);
|
|
786
|
+
const requests = collectCursorBridgeToolBatch(run);
|
|
787
|
+
emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
|
|
491
788
|
return;
|
|
492
789
|
}
|
|
493
790
|
run.pendingEvents.shift();
|
|
494
|
-
|
|
791
|
+
emitCursorLiveQueuedEvent(turn, event, run);
|
|
495
792
|
}
|
|
496
793
|
|
|
497
794
|
if (run.cancelled) {
|
|
498
795
|
partial.stopReason = "aborted";
|
|
499
796
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
500
|
-
await
|
|
797
|
+
await releaseCursorLiveRun(run);
|
|
501
798
|
return;
|
|
502
799
|
}
|
|
503
800
|
if (run.errorMessage) {
|
|
504
801
|
partial.stopReason = "error";
|
|
505
802
|
partial.errorMessage = run.errorMessage;
|
|
506
803
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
507
|
-
await
|
|
804
|
+
await releaseCursorLiveRun(run);
|
|
508
805
|
return;
|
|
509
806
|
}
|
|
510
807
|
if (run.done) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
808
|
+
closeCursorNativeTurnBlocks(turn);
|
|
809
|
+
const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
|
|
810
|
+
if (finalText) {
|
|
811
|
+
await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
|
|
514
812
|
}
|
|
515
|
-
|
|
813
|
+
applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
|
|
516
814
|
partial.stopReason = "stop";
|
|
517
815
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
518
|
-
await
|
|
816
|
+
await releaseCursorLiveRun(run);
|
|
519
817
|
return;
|
|
520
818
|
}
|
|
521
819
|
|
|
@@ -523,21 +821,22 @@ async function emitCursorNativeRunNextTurn(
|
|
|
523
821
|
}
|
|
524
822
|
}
|
|
525
823
|
|
|
526
|
-
async function
|
|
824
|
+
async function replayPendingCursorLiveRun(
|
|
527
825
|
stream: AssistantMessageEventStream,
|
|
528
826
|
partial: AssistantMessage,
|
|
827
|
+
model: Model<Api>,
|
|
529
828
|
context: Context,
|
|
530
829
|
signal?: AbortSignal,
|
|
531
830
|
): Promise<boolean> {
|
|
532
|
-
const
|
|
533
|
-
if (!replayId) return false;
|
|
534
|
-
const run = pendingCursorNativeRuns.get(replayId);
|
|
831
|
+
const run = getPendingCursorLiveRun(context);
|
|
535
832
|
if (!run) return false;
|
|
536
833
|
clearCursorNativeRunIdleDispose(run);
|
|
834
|
+
const consumed = consumeCursorLiveRunToolResults(run, context);
|
|
835
|
+
run.bridgeRun?.resolveToolResults(consumed.toolResults);
|
|
537
836
|
try {
|
|
538
|
-
await emitCursorNativeRunNextTurn(stream, partial, run, signal);
|
|
837
|
+
await emitCursorNativeRunNextTurn(stream, partial, model, context, run, consumed.toolResultInputTokens, signal);
|
|
539
838
|
} catch (error) {
|
|
540
|
-
if (error instanceof CursorAbortError) await
|
|
839
|
+
if (error instanceof CursorAbortError) await releaseCursorLiveRun(run);
|
|
541
840
|
throw error;
|
|
542
841
|
}
|
|
543
842
|
return true;
|
|
@@ -553,10 +852,15 @@ export function streamCursor(
|
|
|
553
852
|
(async () => {
|
|
554
853
|
const partial = makeInitialMessage(model);
|
|
555
854
|
let agent: SDKAgent | null = null;
|
|
556
|
-
let
|
|
855
|
+
let activeLiveRun: CursorLiveRun | undefined;
|
|
856
|
+
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
857
|
+
let liveRunForBridgeQueue: CursorLiveRun | undefined;
|
|
858
|
+
const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
|
|
557
859
|
let resolvedApiKey: string | undefined;
|
|
860
|
+
let sessionAgentScopeKey = "";
|
|
558
861
|
let abortSignal: AbortSignal | undefined;
|
|
559
862
|
let abortListener: (() => void) | undefined;
|
|
863
|
+
let restoreCursorSdkOutputFilter: (() => void) | undefined;
|
|
560
864
|
|
|
561
865
|
try {
|
|
562
866
|
const throwIfAborted = (): void => {
|
|
@@ -566,7 +870,7 @@ export function streamCursor(
|
|
|
566
870
|
stream.push({ type: "start", partial });
|
|
567
871
|
throwIfAborted();
|
|
568
872
|
|
|
569
|
-
if (await
|
|
873
|
+
if (await replayPendingCursorLiveRun(stream, partial, model, context, options?.signal)) {
|
|
570
874
|
stream.end();
|
|
571
875
|
return;
|
|
572
876
|
}
|
|
@@ -575,26 +879,48 @@ export function streamCursor(
|
|
|
575
879
|
if (!apiKey) throw new Error(MISSING_API_KEY_MESSAGE);
|
|
576
880
|
resolvedApiKey = apiKey;
|
|
577
881
|
|
|
578
|
-
// pi-ai Context/SimpleStreamOptions do not
|
|
579
|
-
//
|
|
580
|
-
const cwd =
|
|
882
|
+
// pi-ai Context/SimpleStreamOptions do not expose ExtensionContext.cwd; bridge via session_start
|
|
883
|
+
// until pi threads session cwd into streamSimple (cwd can change without a new session event).
|
|
884
|
+
const cwd = getCursorSessionCwd();
|
|
581
885
|
const fastEnabled = getEffectiveFastForModelId(model.id);
|
|
582
886
|
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
583
887
|
const settingSources = resolveCursorSettingSources();
|
|
584
888
|
|
|
585
|
-
|
|
889
|
+
installCursorMcpToolTimeoutOverride();
|
|
890
|
+
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
891
|
+
const sessionAgentAcquireParams = {
|
|
586
892
|
apiKey,
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
893
|
+
cwd,
|
|
894
|
+
modelSelection: selection,
|
|
895
|
+
settingSources,
|
|
896
|
+
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
897
|
+
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
898
|
+
queueCursorNativeEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
899
|
+
} else {
|
|
900
|
+
queuedBridgeRequestsBeforeLiveRun.push(request);
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
createAgent: (createOptions: Parameters<typeof Agent.create>[0]) =>
|
|
904
|
+
suppressCursorSdkOutput(() => Agent.create(createOptions)),
|
|
905
|
+
};
|
|
906
|
+
let sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
907
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
908
|
+
agent = sessionAgentLease.agent;
|
|
909
|
+
bridgeRun = sessionAgentLease.bridgeRun;
|
|
590
910
|
throwIfAborted();
|
|
591
911
|
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
912
|
+
const promptOptions = getCursorPromptOptions(model);
|
|
913
|
+
let { prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState);
|
|
914
|
+
if (sessionAgentLease.sendState.bootstrapped && bootstrap) {
|
|
915
|
+
await resetSessionCursorAgent(sessionAgentLease.scopeKey);
|
|
916
|
+
sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
917
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
918
|
+
agent = sessionAgentLease.agent;
|
|
919
|
+
bridgeRun = sessionAgentLease.bridgeRun;
|
|
920
|
+
({ prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState));
|
|
921
|
+
}
|
|
922
|
+
const sessionBridgeRun = sessionAgentLease.bridgeRun;
|
|
923
|
+
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
598
924
|
let thinkingContentIndex = -1;
|
|
599
925
|
let activityTraceChars = 0;
|
|
600
926
|
let activityTraceTruncated = false;
|
|
@@ -604,14 +930,18 @@ export function streamCursor(
|
|
|
604
930
|
const nativeReplayId = createCursorNativeReplayId();
|
|
605
931
|
const textDeltas: string[] = [];
|
|
606
932
|
let nativeToolReplayStarted = false;
|
|
607
|
-
const
|
|
933
|
+
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
934
|
+
const liveRun: CursorLiveRun | undefined = useLiveRun
|
|
608
935
|
? {
|
|
609
|
-
id: nativeReplayId,
|
|
936
|
+
id: useNativeToolReplay ? nativeReplayId : bridgeRun!.id,
|
|
610
937
|
agent,
|
|
611
|
-
|
|
612
|
-
|
|
938
|
+
bridgeRun,
|
|
939
|
+
sessionBridgeRun,
|
|
940
|
+
sessionAgentScopeKey,
|
|
941
|
+
accounting: createCursorLiveRunAccountingState(promptInputTokens),
|
|
613
942
|
pendingEvents: [],
|
|
614
943
|
textDeltas,
|
|
944
|
+
emittedText: "",
|
|
615
945
|
recordedToolDisplayIds: [],
|
|
616
946
|
done: false,
|
|
617
947
|
cancelled: false,
|
|
@@ -620,11 +950,20 @@ export function streamCursor(
|
|
|
620
950
|
}
|
|
621
951
|
: undefined;
|
|
622
952
|
if (liveRun) {
|
|
623
|
-
|
|
624
|
-
|
|
953
|
+
pendingCursorLiveRuns.set(liveRun.id, liveRun);
|
|
954
|
+
activeLiveRun = liveRun;
|
|
955
|
+
liveRunForBridgeQueue = liveRun;
|
|
956
|
+
for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
|
|
957
|
+
queueCursorNativeEvent(liveRun, { type: "bridge-tool", request });
|
|
958
|
+
}
|
|
625
959
|
}
|
|
626
960
|
const startedToolCalls = new Map<string, unknown>();
|
|
961
|
+
const bridgeStartedToolCallIds = new Set<string>();
|
|
962
|
+
const activeShellCallIds = new Set<string>();
|
|
963
|
+
const ambiguousShellOutputCallIds = new Set<string>();
|
|
964
|
+
const shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
|
|
627
965
|
const completedToolIdentities = new Set<string>();
|
|
966
|
+
let cursorPlanTextCandidate: string | undefined;
|
|
628
967
|
const completedStartedToolFingerprints = new Set<string>();
|
|
629
968
|
const completedFallbackToolFingerprints = new Set<string>();
|
|
630
969
|
|
|
@@ -684,6 +1023,16 @@ export function streamCursor(
|
|
|
684
1023
|
closeTraceBlock();
|
|
685
1024
|
};
|
|
686
1025
|
|
|
1026
|
+
const emitCursorToolTrace = (text: string): void => {
|
|
1027
|
+
const traceText = text.endsWith("\n") ? text : `${text}\n`;
|
|
1028
|
+
if (liveRun) {
|
|
1029
|
+
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: traceText });
|
|
1030
|
+
queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
appendTraceBlock(traceText);
|
|
1034
|
+
};
|
|
1035
|
+
|
|
687
1036
|
const closeTraceBlock = (): void => {
|
|
688
1037
|
if (thinkingContentIndex < 0) return;
|
|
689
1038
|
const block = partial.content[thinkingContentIndex];
|
|
@@ -720,10 +1069,80 @@ export function streamCursor(
|
|
|
720
1069
|
}
|
|
721
1070
|
};
|
|
722
1071
|
|
|
1072
|
+
const getStartedToolCallFingerprint = (toolCall: unknown): string => {
|
|
1073
|
+
return getToolFingerprint({ toolName: getCursorToolName(toolCall), args: getObjectField(toolCall, "args") });
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const clearStartedToolCall = (callId: string): void => {
|
|
1077
|
+
startedToolCalls.delete(callId);
|
|
1078
|
+
bridgeStartedToolCallIds.delete(callId);
|
|
1079
|
+
activeShellCallIds.delete(callId);
|
|
1080
|
+
ambiguousShellOutputCallIds.delete(callId);
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const takeBridgeStartedToolCallId = (callId: unknown): string | undefined => {
|
|
1084
|
+
if (typeof callId !== "string" || !bridgeStartedToolCallIds.has(callId)) return undefined;
|
|
1085
|
+
bridgeStartedToolCallIds.delete(callId);
|
|
1086
|
+
return callId;
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
const takeShellOutputDeltas = (callId: string): CursorShellOutputDeltas | undefined => {
|
|
1090
|
+
const deltas = shellOutputDeltasByCallId.get(callId);
|
|
1091
|
+
shellOutputDeltasByCallId.delete(callId);
|
|
1092
|
+
return deltas;
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const appendShellOutputDelta = (delta: CursorShellOutputDelta): void => {
|
|
1096
|
+
if (activeShellCallIds.size !== 1) {
|
|
1097
|
+
for (const activeCallId of activeShellCallIds) {
|
|
1098
|
+
ambiguousShellOutputCallIds.add(activeCallId);
|
|
1099
|
+
shellOutputDeltasByCallId.delete(activeCallId);
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const [callId] = activeShellCallIds;
|
|
1104
|
+
if (!callId || ambiguousShellOutputCallIds.has(callId)) return;
|
|
1105
|
+
let deltas = shellOutputDeltasByCallId.get(callId);
|
|
1106
|
+
if (!deltas) {
|
|
1107
|
+
deltas = { stdout: [], stderr: [] };
|
|
1108
|
+
shellOutputDeltasByCallId.set(callId, deltas);
|
|
1109
|
+
}
|
|
1110
|
+
deltas[delta.stream].push(delta.data);
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
const removeStartedToolCallForStep = (toolCall: unknown, stepId: unknown): string | undefined => {
|
|
1114
|
+
if (typeof stepId === "string" && startedToolCalls.has(stepId)) {
|
|
1115
|
+
clearStartedToolCall(stepId);
|
|
1116
|
+
return stepId;
|
|
1117
|
+
}
|
|
1118
|
+
const fingerprint = getStartedToolCallFingerprint(toolCall);
|
|
1119
|
+
for (const [callId, startedToolCall] of startedToolCalls) {
|
|
1120
|
+
if (getStartedToolCallFingerprint(startedToolCall) !== fingerprint) continue;
|
|
1121
|
+
clearStartedToolCall(callId);
|
|
1122
|
+
return callId;
|
|
1123
|
+
}
|
|
1124
|
+
return undefined;
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const discardIncompleteStartedToolCalls = (): void => {
|
|
1128
|
+
startedToolCalls.clear();
|
|
1129
|
+
bridgeStartedToolCallIds.clear();
|
|
1130
|
+
activeShellCallIds.clear();
|
|
1131
|
+
ambiguousShellOutputCallIds.clear();
|
|
1132
|
+
shellOutputDeltasByCallId.clear();
|
|
1133
|
+
};
|
|
1134
|
+
|
|
723
1135
|
const handleCompletedToolCall = (
|
|
724
1136
|
toolCall: unknown,
|
|
725
1137
|
options: { identity?: string; source?: "started" | "fallback" } = {},
|
|
726
1138
|
): void => {
|
|
1139
|
+
const planText = getCursorCreatePlanText(toolCall);
|
|
1140
|
+
if (planText) cursorPlanTextCandidate = scrubSensitiveText(planText, resolvedApiKey);
|
|
1141
|
+
|
|
1142
|
+
if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
|
|
1143
|
+
if (options.identity) completedToolIdentities.add(options.identity);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
727
1146
|
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
|
|
728
1147
|
const display = buildCursorPiToolDisplay(toolCall, { cwd });
|
|
729
1148
|
const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
|
|
@@ -740,11 +1159,10 @@ export function streamCursor(
|
|
|
740
1159
|
completedFallbackToolFingerprints.add(fingerprint);
|
|
741
1160
|
}
|
|
742
1161
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
1162
|
+
const nativeRenderable = canRenderCursorToolNatively(display.toolName);
|
|
1163
|
+
const route = useNativeToolReplay && nativeRenderable && liveRun ? "native_replay" : "trace";
|
|
1164
|
+
|
|
1165
|
+
if (route === "native_replay" && liveRun) {
|
|
748
1166
|
nativeToolReplayStarted = true;
|
|
749
1167
|
const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
|
|
750
1168
|
queueCursorNativeEvent(liveRun, {
|
|
@@ -759,7 +1177,7 @@ export function streamCursor(
|
|
|
759
1177
|
return;
|
|
760
1178
|
}
|
|
761
1179
|
|
|
762
|
-
|
|
1180
|
+
emitCursorToolTrace(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
|
|
763
1181
|
};
|
|
764
1182
|
|
|
765
1183
|
const onDelta = (args: { update: InteractionUpdate }): void => {
|
|
@@ -767,36 +1185,50 @@ export function streamCursor(
|
|
|
767
1185
|
|
|
768
1186
|
if (update.type === "text-delta") {
|
|
769
1187
|
textDeltas.push(update.text);
|
|
770
|
-
if (liveRun
|
|
1188
|
+
if (liveRun) {
|
|
771
1189
|
queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
|
|
772
|
-
} else
|
|
1190
|
+
} else {
|
|
773
1191
|
appendLiveTextDelta(update.text);
|
|
774
1192
|
}
|
|
775
1193
|
} else if (update.type === "thinking-delta") {
|
|
776
|
-
if (liveRun
|
|
1194
|
+
if (liveRun) {
|
|
777
1195
|
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
|
|
778
1196
|
} else {
|
|
779
1197
|
appendTraceDelta(update.text);
|
|
780
1198
|
}
|
|
781
1199
|
} else if (update.type === "thinking-completed") {
|
|
782
|
-
if (liveRun
|
|
1200
|
+
if (liveRun) {
|
|
783
1201
|
queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
|
|
784
1202
|
} else {
|
|
785
1203
|
closeTraceBlock();
|
|
786
1204
|
}
|
|
787
1205
|
} else if (update.type === "tool-call-started") {
|
|
788
|
-
|
|
1206
|
+
if (liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
|
|
1207
|
+
if (typeof update.callId === "string") bridgeStartedToolCallIds.add(update.callId);
|
|
1208
|
+
} else {
|
|
1209
|
+
startedToolCalls.set(update.callId, update.toolCall);
|
|
1210
|
+
if (isCursorShellToolCall(update.toolCall)) activeShellCallIds.add(update.callId);
|
|
1211
|
+
}
|
|
789
1212
|
} else if (update.type === "tool-call-completed") {
|
|
790
|
-
const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
|
|
791
|
-
startedToolCalls.delete(update.callId);
|
|
792
1213
|
const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
|
|
793
|
-
|
|
1214
|
+
const bridgeStartedCallId = takeBridgeStartedToolCallId(update.callId);
|
|
1215
|
+
if (bridgeStartedCallId) {
|
|
1216
|
+
completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
|
|
1220
|
+
clearStartedToolCall(update.callId);
|
|
1221
|
+
const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(mergedToolCall, takeShellOutputDeltas(update.callId));
|
|
1222
|
+
handleCompletedToolCall(toolCallWithShellOutput, {
|
|
794
1223
|
identity,
|
|
795
1224
|
source: identity ? "started" : "fallback",
|
|
796
1225
|
});
|
|
1226
|
+
} else if (update.type === "shell-output-delta") {
|
|
1227
|
+
const delta = getCursorShellOutputDelta(update);
|
|
1228
|
+
if (delta) appendShellOutputDelta(delta);
|
|
797
1229
|
} else if (update.type === "summary") {
|
|
798
1230
|
const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
|
|
799
|
-
if (liveRun
|
|
1231
|
+
if (liveRun) {
|
|
800
1232
|
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
|
|
801
1233
|
} else {
|
|
802
1234
|
appendTraceDelta(summary);
|
|
@@ -805,24 +1237,43 @@ export function streamCursor(
|
|
|
805
1237
|
// Cursor turn-ended usage is intentionally not copied into pi usage: the SDK reports
|
|
806
1238
|
// cumulative internal agent/tool/cache tokens, not the replayable pi prompt context.
|
|
807
1239
|
// partial-tool-call, summary-started, summary-completed, turn-ended,
|
|
808
|
-
//
|
|
1240
|
+
// token-delta, step-* are intentionally not surfaced.
|
|
809
1241
|
};
|
|
810
1242
|
|
|
811
1243
|
const onStep = (args: { step: unknown }): void => {
|
|
1244
|
+
const stepType = getObjectField(args.step, "type");
|
|
812
1245
|
const step = getObjectField(args.step, "message") ? args.step : undefined;
|
|
813
|
-
|
|
814
|
-
|
|
1246
|
+
const rawStepToolCall = getObjectField(step, "message");
|
|
1247
|
+
if (stepType !== "toolCall") return;
|
|
1248
|
+
const toolCall = rawStepToolCall;
|
|
815
1249
|
const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
|
|
816
1250
|
if (toolCall) {
|
|
817
|
-
|
|
818
|
-
|
|
1251
|
+
const bridgeStartedCallId = takeBridgeStartedToolCallId(stepId);
|
|
1252
|
+
if (bridgeStartedCallId) {
|
|
1253
|
+
completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const matchedStartedCallId = removeStartedToolCallForStep(toolCall, stepId);
|
|
1257
|
+
const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(
|
|
1258
|
+
toolCall,
|
|
1259
|
+
matchedStartedCallId ? takeShellOutputDeltas(matchedStartedCallId) : undefined,
|
|
1260
|
+
);
|
|
1261
|
+
if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
|
|
1262
|
+
if (matchedStartedCallId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const identityId = typeof stepId === "string" ? stepId : matchedStartedCallId;
|
|
1266
|
+
handleCompletedToolCall(toolCallWithShellOutput, {
|
|
1267
|
+
identity: identityId ? `cursor-tool:${identityId}` : undefined,
|
|
819
1268
|
});
|
|
1269
|
+
if (matchedStartedCallId && matchedStartedCallId !== stepId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
|
|
820
1270
|
}
|
|
821
1271
|
};
|
|
822
1272
|
|
|
823
1273
|
// Handle abort signal
|
|
824
1274
|
let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
|
|
825
1275
|
abortListener = () => {
|
|
1276
|
+
activeLiveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
826
1277
|
if (run) {
|
|
827
1278
|
run.cancel().catch(() => {});
|
|
828
1279
|
}
|
|
@@ -835,26 +1286,38 @@ export function streamCursor(
|
|
|
835
1286
|
{ text: prompt.text, images: prompt.images.length > 0 ? prompt.images : undefined },
|
|
836
1287
|
{ onDelta, onStep },
|
|
837
1288
|
);
|
|
1289
|
+
if (liveRun) liveRun.sdkRun = run;
|
|
838
1290
|
if (options?.signal?.aborted) {
|
|
839
1291
|
await run.cancel().catch(() => {});
|
|
840
1292
|
throw new CursorAbortError();
|
|
841
1293
|
}
|
|
842
1294
|
|
|
843
|
-
if (
|
|
1295
|
+
if (liveRun) {
|
|
844
1296
|
void run
|
|
845
1297
|
.wait()
|
|
846
1298
|
.then(async (result) => {
|
|
847
1299
|
if (liveRun.disposed) return;
|
|
1300
|
+
discardIncompleteStartedToolCalls();
|
|
848
1301
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
849
1302
|
if (liveRun.disposed) return;
|
|
1303
|
+
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
1304
|
+
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
1305
|
+
} else {
|
|
1306
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1307
|
+
}
|
|
850
1308
|
liveRun.cancelled = result.status === "cancelled";
|
|
851
|
-
|
|
1309
|
+
if (result.status === "error") {
|
|
1310
|
+
liveRun.errorMessage = sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey);
|
|
1311
|
+
} else {
|
|
1312
|
+
liveRun.finalText = selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, cursorPlanTextCandidate);
|
|
1313
|
+
}
|
|
852
1314
|
liveRun.done = true;
|
|
853
1315
|
notifyCursorNativeRun(liveRun);
|
|
854
1316
|
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
855
1317
|
})
|
|
856
|
-
.catch((error: unknown) => {
|
|
1318
|
+
.catch(async (error: unknown) => {
|
|
857
1319
|
if (liveRun.disposed) return;
|
|
1320
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
858
1321
|
liveRun.errorMessage = sanitizeError(error, resolvedApiKey ?? options?.apiKey);
|
|
859
1322
|
notifyCursorNativeRun(liveRun);
|
|
860
1323
|
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
@@ -862,11 +1325,11 @@ export function streamCursor(
|
|
|
862
1325
|
|
|
863
1326
|
try {
|
|
864
1327
|
await waitForCursorNativeRunProgress(liveRun, options?.signal);
|
|
865
|
-
await
|
|
1328
|
+
await settleCursorLiveToolBatch(liveRun);
|
|
866
1329
|
closeTraceBlock();
|
|
867
|
-
await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
|
|
1330
|
+
await emitCursorNativeRunNextTurn(stream, partial, model, context, liveRun, 0, options?.signal);
|
|
868
1331
|
} catch (error) {
|
|
869
|
-
if (error instanceof CursorAbortError) await
|
|
1332
|
+
if (error instanceof CursorAbortError) await releaseCursorLiveRun(liveRun);
|
|
870
1333
|
throw error;
|
|
871
1334
|
}
|
|
872
1335
|
agent = null;
|
|
@@ -874,6 +1337,7 @@ export function streamCursor(
|
|
|
874
1337
|
}
|
|
875
1338
|
|
|
876
1339
|
const result = await run.wait();
|
|
1340
|
+
discardIncompleteStartedToolCalls();
|
|
877
1341
|
await cacheSdkContextWindow(agent.agentId, model.id);
|
|
878
1342
|
|
|
879
1343
|
// Close any open thinking/activity trace, then use the final run result only when
|
|
@@ -881,14 +1345,26 @@ export function streamCursor(
|
|
|
881
1345
|
closeTraceBlock();
|
|
882
1346
|
|
|
883
1347
|
if (result.status === "cancelled") {
|
|
1348
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
884
1349
|
partial.stopReason = "aborted";
|
|
885
1350
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
1351
|
+
} else if (result.status === "error") {
|
|
1352
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1353
|
+
partial.stopReason = "error";
|
|
1354
|
+
partial.errorMessage = sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey);
|
|
1355
|
+
stream.push({ type: "error", reason: "error", error: partial });
|
|
886
1356
|
} else {
|
|
887
|
-
|
|
888
|
-
|
|
1357
|
+
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
1358
|
+
const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""), cursorPlanTextCandidate, {
|
|
1359
|
+
allowPartialPrefix: true,
|
|
1360
|
+
});
|
|
1361
|
+
flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
|
|
1362
|
+
applyCursorApproximateUsage(partial, model, context, promptInputTokens);
|
|
889
1363
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
890
1364
|
}
|
|
891
1365
|
} catch (error) {
|
|
1366
|
+
if (activeLiveRun && !activeLiveRun.disposed) await releaseCursorLiveRun(activeLiveRun);
|
|
1367
|
+
else await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
892
1368
|
if (error instanceof CursorAbortError) {
|
|
893
1369
|
partial.stopReason = "aborted";
|
|
894
1370
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
@@ -898,20 +1374,11 @@ export function streamCursor(
|
|
|
898
1374
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
899
1375
|
}
|
|
900
1376
|
} finally {
|
|
901
|
-
|
|
1377
|
+
restoreCursorSdkOutputFilter?.();
|
|
902
1378
|
|
|
903
1379
|
if (abortSignal && abortListener) {
|
|
904
1380
|
abortSignal.removeEventListener("abort", abortListener);
|
|
905
1381
|
}
|
|
906
|
-
|
|
907
|
-
if (agent) {
|
|
908
|
-
try {
|
|
909
|
-
await agent[Symbol.asyncDispose]();
|
|
910
|
-
} catch {
|
|
911
|
-
// disposal failure should not mask original error
|
|
912
|
-
}
|
|
913
|
-
agent = null;
|
|
914
|
-
}
|
|
915
1382
|
}
|
|
916
1383
|
|
|
917
1384
|
stream.end();
|
|
@@ -922,11 +1389,12 @@ export function streamCursor(
|
|
|
922
1389
|
|
|
923
1390
|
export const __testUtils = {
|
|
924
1391
|
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
925
|
-
pendingCursorNativeRunCount: () =>
|
|
1392
|
+
pendingCursorNativeRunCount: () => pendingCursorLiveRuns.size,
|
|
926
1393
|
setCursorNativeReplayIdleDisposeMs: (value: number) => {
|
|
927
1394
|
cursorNativeReplayIdleDisposeMs = value;
|
|
928
1395
|
},
|
|
929
1396
|
resetCursorNativeReplayIdleDisposeMs: () => {
|
|
930
1397
|
cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
931
1398
|
},
|
|
1399
|
+
resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
|
|
932
1400
|
};
|