pi-cursor-sdk 0.1.14 → 0.1.15
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 +30 -0
- package/README.md +55 -13
- package/docs/cursor-model-ux-spec.md +17 -3
- package/docs/cursor-native-tool-replay.md +88 -0
- package/docs/cursor-native-tool-visual-audit.md +183 -0
- package/package.json +5 -2
- package/src/context.ts +34 -11
- package/src/cursor-mcp-timeout-override.ts +111 -0
- package/src/cursor-native-tool-display.ts +397 -46
- package/src/cursor-pi-tool-bridge.ts +637 -0
- package/src/cursor-provider.ts +477 -81
- package/src/cursor-question-tool.ts +247 -0
- package/src/cursor-session-cwd.ts +33 -0
- package/src/cursor-tool-names.ts +67 -0
- package/src/cursor-tool-transcript.ts +730 -61
- package/src/index.ts +7 -0
package/src/cursor-provider.ts
CHANGED
|
@@ -7,13 +7,21 @@ import {
|
|
|
7
7
|
type SimpleStreamOptions,
|
|
8
8
|
type AssistantMessage,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
11
|
import { Agent, createAgentPlatform } from "@cursor/sdk";
|
|
11
12
|
import type { InteractionUpdate, SDKAgent, SettingSource } from "@cursor/sdk";
|
|
13
|
+
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
12
14
|
import { buildCursorPrompt, type CursorPrompt } from "./context.js";
|
|
15
|
+
import {
|
|
16
|
+
getRegisteredCursorPiToolBridge,
|
|
17
|
+
type CursorPiBridgeToolRequest,
|
|
18
|
+
type CursorPiToolBridgeRun,
|
|
19
|
+
} from "./cursor-pi-tool-bridge.js";
|
|
20
|
+
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
13
21
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
14
22
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
15
23
|
import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
|
|
16
|
-
import { buildCursorPiToolDisplay, formatCursorToolTranscript, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
|
|
24
|
+
import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
|
|
17
25
|
import {
|
|
18
26
|
canRenderCursorToolNatively,
|
|
19
27
|
isCursorNativeToolDisplayRuntimeEnabled,
|
|
@@ -62,20 +70,24 @@ const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
|
|
|
62
70
|
const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
63
71
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
64
72
|
const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
73
|
+
const cursorSdkOutputSuppression = new AsyncLocalStorage<boolean>();
|
|
65
74
|
|
|
66
|
-
type
|
|
75
|
+
type CursorLiveQueuedEvent =
|
|
67
76
|
| { type: "thinking-delta"; text: string }
|
|
68
77
|
| { type: "thinking-completed" }
|
|
69
78
|
| { type: "text-delta"; text: string }
|
|
70
|
-
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
79
|
+
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
80
|
+
| { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
|
|
71
81
|
|
|
72
|
-
interface
|
|
82
|
+
interface CursorLiveRun {
|
|
73
83
|
id: string;
|
|
74
84
|
agent: SDKAgent;
|
|
85
|
+
bridgeRun?: CursorPiToolBridgeRun;
|
|
75
86
|
promptInputTokens: number;
|
|
76
87
|
promptInputTokensReported: boolean;
|
|
77
|
-
pendingEvents:
|
|
88
|
+
pendingEvents: CursorLiveQueuedEvent[];
|
|
78
89
|
textDeltas: string[];
|
|
90
|
+
emittedText: string;
|
|
79
91
|
recordedToolDisplayIds: string[];
|
|
80
92
|
finalText?: string;
|
|
81
93
|
done: boolean;
|
|
@@ -86,7 +98,7 @@ interface CursorNativeLiveRun {
|
|
|
86
98
|
waiters: Set<() => void>;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
interface
|
|
101
|
+
interface CursorLiveTurnState {
|
|
90
102
|
stream: AssistantMessageEventStream;
|
|
91
103
|
partial: AssistantMessage;
|
|
92
104
|
thinkingContentIndex: number;
|
|
@@ -95,7 +107,7 @@ interface CursorNativeTurnState {
|
|
|
95
107
|
|
|
96
108
|
let cursorNativeReplayCounter = 0;
|
|
97
109
|
let cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
98
|
-
const
|
|
110
|
+
const pendingCursorLiveRuns = new Map<string, CursorLiveRun>();
|
|
99
111
|
|
|
100
112
|
function escapeRegExp(value: string): string {
|
|
101
113
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -134,7 +146,7 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
|
134
146
|
|
|
135
147
|
function resolveCursorSettingSources(): SettingSource[] | undefined {
|
|
136
148
|
const raw = process.env[CURSOR_SETTING_SOURCES_ENV]?.trim();
|
|
137
|
-
if (!raw) return
|
|
149
|
+
if (!raw) return ["all"];
|
|
138
150
|
const normalized = raw.toLowerCase();
|
|
139
151
|
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
140
152
|
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
@@ -153,6 +165,78 @@ function sanitizeError(error: unknown, apiKey?: string): string {
|
|
|
153
165
|
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
154
166
|
}
|
|
155
167
|
|
|
168
|
+
function isCursorSdkOutputSuppressed(): boolean {
|
|
169
|
+
return cursorSdkOutputSuppression.getStore() === true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function suppressCursorSdkOutput<T>(operation: () => Promise<T>): Promise<T> {
|
|
173
|
+
return cursorSdkOutputSuppression.run(true, operation);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const CURSOR_SDK_STARTUP_NOISE_PATTERNS = [
|
|
177
|
+
"managed_skills.",
|
|
178
|
+
"CursorPluginsAgentSkillsService load completed",
|
|
179
|
+
"LocalCursorRulesService load completed",
|
|
180
|
+
"AgentSkillsCursorRulesService load completed",
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
function isCursorSdkStartupNoise(text: string): boolean {
|
|
184
|
+
return CURSOR_SDK_STARTUP_NOISE_PATTERNS.some((pattern) => text.includes(pattern));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createFilteredProcessWrite<TWrite extends typeof process.stdout.write>(write: TWrite, stream: NodeJS.WriteStream): TWrite {
|
|
188
|
+
return ((
|
|
189
|
+
chunk: string | Uint8Array,
|
|
190
|
+
encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
|
|
191
|
+
callback?: (error?: Error | null) => void,
|
|
192
|
+
): boolean => {
|
|
193
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
194
|
+
if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) {
|
|
195
|
+
const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
196
|
+
done?.();
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return write.call(stream, chunk as string, encodingOrCallback as BufferEncoding, callback);
|
|
200
|
+
}) as TWrite;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createFilteredConsoleMethod<TMethod extends typeof console.log>(method: TMethod): TMethod {
|
|
204
|
+
return ((...args: Parameters<TMethod>): void => {
|
|
205
|
+
const text = args.map((arg) => (typeof arg === "string" ? arg : String(arg))).join(" ");
|
|
206
|
+
if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) return;
|
|
207
|
+
method(...args);
|
|
208
|
+
}) as TMethod;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function installCursorSdkOutputFilter(): () => void {
|
|
212
|
+
const stdoutWrite = process.stdout.write;
|
|
213
|
+
const stderrWrite = process.stderr.write;
|
|
214
|
+
const consoleLog = console.log;
|
|
215
|
+
const consoleInfo = console.info;
|
|
216
|
+
const consoleWarn = console.warn;
|
|
217
|
+
const consoleError = console.error;
|
|
218
|
+
const consoleDebug = console.debug;
|
|
219
|
+
process.stdout.write = createFilteredProcessWrite(stdoutWrite, process.stdout);
|
|
220
|
+
process.stderr.write = createFilteredProcessWrite(stderrWrite, process.stderr) as typeof process.stderr.write;
|
|
221
|
+
console.log = createFilteredConsoleMethod(consoleLog);
|
|
222
|
+
console.info = createFilteredConsoleMethod(consoleInfo);
|
|
223
|
+
console.warn = createFilteredConsoleMethod(consoleWarn);
|
|
224
|
+
console.error = createFilteredConsoleMethod(consoleError);
|
|
225
|
+
console.debug = createFilteredConsoleMethod(consoleDebug);
|
|
226
|
+
let restored = false;
|
|
227
|
+
return () => {
|
|
228
|
+
if (restored) return;
|
|
229
|
+
restored = true;
|
|
230
|
+
process.stdout.write = stdoutWrite;
|
|
231
|
+
process.stderr.write = stderrWrite;
|
|
232
|
+
console.log = consoleLog;
|
|
233
|
+
console.info = consoleInfo;
|
|
234
|
+
console.warn = consoleWarn;
|
|
235
|
+
console.error = consoleError;
|
|
236
|
+
console.debug = consoleDebug;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
156
240
|
function getObjectField(value: unknown, field: string): unknown {
|
|
157
241
|
if (!value || typeof value !== "object") return undefined;
|
|
158
242
|
return (value as Record<string, unknown>)[field];
|
|
@@ -163,6 +247,7 @@ function getCursorToolName(toolCall: unknown): string {
|
|
|
163
247
|
const data = toolCall as Record<string, unknown>;
|
|
164
248
|
if (typeof data.name === "string") return data.name;
|
|
165
249
|
if (typeof data.type === "string") return data.type;
|
|
250
|
+
if (typeof data.toolName === "string") return data.toolName;
|
|
166
251
|
return "unknown";
|
|
167
252
|
}
|
|
168
253
|
|
|
@@ -215,6 +300,63 @@ function hasUsableText(value: string | undefined): value is string {
|
|
|
215
300
|
return typeof value === "string" && value.trim().length > 0;
|
|
216
301
|
}
|
|
217
302
|
|
|
303
|
+
interface CursorShellOutputDelta {
|
|
304
|
+
stream: "stdout" | "stderr";
|
|
305
|
+
data: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
interface CursorShellOutputDeltas {
|
|
309
|
+
stdout: string[];
|
|
310
|
+
stderr: string[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isCursorShellToolCall(toolCall: unknown): boolean {
|
|
314
|
+
const normalizedName = getCursorToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase();
|
|
315
|
+
return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
|
|
319
|
+
if (update.type !== "shell-output-delta") return undefined;
|
|
320
|
+
const event = getObjectField(update, "event");
|
|
321
|
+
const eventCase = getObjectField(event, "case");
|
|
322
|
+
if (eventCase !== "stdout" && eventCase !== "stderr") return undefined;
|
|
323
|
+
const value = getObjectField(event, "value");
|
|
324
|
+
const data = getObjectField(value, "data");
|
|
325
|
+
if (typeof data !== "string" || data.length === 0) return undefined;
|
|
326
|
+
return { stream: eventCase, data };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: CursorShellOutputDeltas | undefined): unknown {
|
|
330
|
+
if (!deltas) return toolCall;
|
|
331
|
+
const stdout = deltas.stdout.join("");
|
|
332
|
+
const stderr = deltas.stderr.join("");
|
|
333
|
+
if (!hasUsableText(stdout) && !hasUsableText(stderr)) return toolCall;
|
|
334
|
+
|
|
335
|
+
const toolRecord = toolCall && typeof toolCall === "object" && !Array.isArray(toolCall) ? (toolCall as Record<string, unknown>) : undefined;
|
|
336
|
+
const result = getObjectField(toolRecord, "result");
|
|
337
|
+
const resultRecord = result && typeof result === "object" && !Array.isArray(result) ? (result as Record<string, unknown>) : undefined;
|
|
338
|
+
if (!toolRecord || !resultRecord || resultRecord.status !== "success") return toolCall;
|
|
339
|
+
|
|
340
|
+
const value = getObjectField(resultRecord, "value");
|
|
341
|
+
const valueRecord = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
342
|
+
const completedStdout = getObjectField(valueRecord, "stdout");
|
|
343
|
+
const completedStderr = getObjectField(valueRecord, "stderr");
|
|
344
|
+
if (hasUsableText(typeof completedStdout === "string" ? completedStdout : undefined)) return toolCall;
|
|
345
|
+
if (hasUsableText(typeof completedStderr === "string" ? completedStderr : undefined)) return toolCall;
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
...toolRecord,
|
|
349
|
+
result: {
|
|
350
|
+
...resultRecord,
|
|
351
|
+
value: {
|
|
352
|
+
...(valueRecord ?? {}),
|
|
353
|
+
stdout,
|
|
354
|
+
stderr,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
218
360
|
function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
|
|
219
361
|
if (typeof value === "string") return scrubSensitiveText(value, apiKey);
|
|
220
362
|
if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
|
|
@@ -233,12 +375,18 @@ function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | und
|
|
|
233
375
|
return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
|
|
234
376
|
}
|
|
235
377
|
|
|
236
|
-
function
|
|
378
|
+
function getPendingCursorLiveRun(context: Context): CursorLiveRun | undefined {
|
|
237
379
|
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
|
238
380
|
const message = context.messages[index];
|
|
239
381
|
if (message.role !== "toolResult") break;
|
|
240
382
|
const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
|
|
241
|
-
if (replayId
|
|
383
|
+
if (replayId) {
|
|
384
|
+
const replayRun = pendingCursorLiveRuns.get(replayId);
|
|
385
|
+
if (replayRun) return replayRun;
|
|
386
|
+
}
|
|
387
|
+
for (const run of pendingCursorLiveRuns.values()) {
|
|
388
|
+
if (run.bridgeRun?.hasPendingPiToolCallId(message.toolCallId)) return run;
|
|
389
|
+
}
|
|
242
390
|
}
|
|
243
391
|
return undefined;
|
|
244
392
|
}
|
|
@@ -278,23 +426,23 @@ async function emitTextDeltas(
|
|
|
278
426
|
return block.text;
|
|
279
427
|
}
|
|
280
428
|
|
|
281
|
-
function notifyCursorNativeRun(run:
|
|
429
|
+
function notifyCursorNativeRun(run: CursorLiveRun): void {
|
|
282
430
|
for (const waiter of run.waiters) waiter();
|
|
283
431
|
run.waiters.clear();
|
|
284
432
|
}
|
|
285
433
|
|
|
286
|
-
function queueCursorNativeEvent(run:
|
|
434
|
+
function queueCursorNativeEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void {
|
|
287
435
|
run.pendingEvents.push(event);
|
|
288
436
|
notifyCursorNativeRun(run);
|
|
289
437
|
}
|
|
290
438
|
|
|
291
|
-
function clearCursorNativeRunIdleDispose(run:
|
|
439
|
+
function clearCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
292
440
|
if (!run.idleDisposeTimer) return;
|
|
293
441
|
clearTimeout(run.idleDisposeTimer);
|
|
294
442
|
run.idleDisposeTimer = undefined;
|
|
295
443
|
}
|
|
296
444
|
|
|
297
|
-
function scheduleCursorNativeRunIdleDispose(run:
|
|
445
|
+
function scheduleCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
298
446
|
if (run.disposed) return;
|
|
299
447
|
clearCursorNativeRunIdleDispose(run);
|
|
300
448
|
run.idleDisposeTimer = setTimeout(() => {
|
|
@@ -303,11 +451,12 @@ function scheduleCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
|
|
|
303
451
|
run.idleDisposeTimer.unref?.();
|
|
304
452
|
}
|
|
305
453
|
|
|
306
|
-
function isCursorNativeRunReady(run:
|
|
454
|
+
function isCursorNativeRunReady(run: CursorLiveRun): boolean {
|
|
307
455
|
return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
|
|
308
456
|
}
|
|
309
457
|
|
|
310
|
-
async function waitForCursorNativeRunProgress(run:
|
|
458
|
+
async function waitForCursorNativeRunProgress(run: CursorLiveRun, signal?: AbortSignal): Promise<void> {
|
|
459
|
+
if (signal?.aborted) throw new CursorAbortError();
|
|
311
460
|
if (isCursorNativeRunReady(run)) return;
|
|
312
461
|
await new Promise<void>((resolve, reject) => {
|
|
313
462
|
let waiter: (() => void) | undefined;
|
|
@@ -324,16 +473,21 @@ async function waitForCursorNativeRunProgress(run: CursorNativeLiveRun, signal?:
|
|
|
324
473
|
resolve();
|
|
325
474
|
};
|
|
326
475
|
run.waiters.add(waiter);
|
|
476
|
+
if (signal?.aborted) {
|
|
477
|
+
onAbort();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
327
480
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
328
481
|
});
|
|
329
482
|
}
|
|
330
483
|
|
|
331
|
-
async function
|
|
332
|
-
|
|
484
|
+
async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<void> {
|
|
485
|
+
const eventType = run.pendingEvents[0]?.type;
|
|
486
|
+
if (eventType !== "tool" && eventType !== "bridge-tool") return;
|
|
333
487
|
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
334
488
|
}
|
|
335
489
|
|
|
336
|
-
function closeCursorNativeThinkingBlock(turn:
|
|
490
|
+
function closeCursorNativeThinkingBlock(turn: CursorLiveTurnState): void {
|
|
337
491
|
if (turn.thinkingContentIndex < 0) return;
|
|
338
492
|
const block = turn.partial.content[turn.thinkingContentIndex];
|
|
339
493
|
if (block.type === "thinking") {
|
|
@@ -347,7 +501,7 @@ function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
|
|
|
347
501
|
turn.thinkingContentIndex = -1;
|
|
348
502
|
}
|
|
349
503
|
|
|
350
|
-
function closeCursorNativeTextBlock(turn:
|
|
504
|
+
function closeCursorNativeTextBlock(turn: CursorLiveTurnState): string {
|
|
351
505
|
if (turn.textContentIndex < 0) return "";
|
|
352
506
|
const contentIndex = turn.textContentIndex;
|
|
353
507
|
const block = turn.partial.content[contentIndex];
|
|
@@ -362,12 +516,12 @@ function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
|
|
|
362
516
|
return block.text;
|
|
363
517
|
}
|
|
364
518
|
|
|
365
|
-
function closeCursorNativeTurnBlocks(turn:
|
|
519
|
+
function closeCursorNativeTurnBlocks(turn: CursorLiveTurnState): string {
|
|
366
520
|
closeCursorNativeThinkingBlock(turn);
|
|
367
521
|
return closeCursorNativeTextBlock(turn);
|
|
368
522
|
}
|
|
369
523
|
|
|
370
|
-
function emitCursorNativeThinkingDelta(turn:
|
|
524
|
+
function emitCursorNativeThinkingDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
371
525
|
closeCursorNativeTextBlock(turn);
|
|
372
526
|
if (turn.thinkingContentIndex < 0) {
|
|
373
527
|
turn.thinkingContentIndex = turn.partial.content.length;
|
|
@@ -380,7 +534,7 @@ function emitCursorNativeThinkingDelta(turn: CursorNativeTurnState, delta: strin
|
|
|
380
534
|
turn.stream.push({ type: "thinking_delta", contentIndex: turn.thinkingContentIndex, delta, partial: turn.partial });
|
|
381
535
|
}
|
|
382
536
|
|
|
383
|
-
function emitCursorNativeTextDelta(turn:
|
|
537
|
+
function emitCursorNativeTextDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
384
538
|
closeCursorNativeThinkingBlock(turn);
|
|
385
539
|
if (turn.textContentIndex < 0) {
|
|
386
540
|
turn.textContentIndex = turn.partial.content.length;
|
|
@@ -393,20 +547,22 @@ function emitCursorNativeTextDelta(turn: CursorNativeTurnState, delta: string):
|
|
|
393
547
|
turn.stream.push({ type: "text_delta", contentIndex: turn.textContentIndex, delta, partial: turn.partial });
|
|
394
548
|
}
|
|
395
549
|
|
|
396
|
-
function
|
|
397
|
-
turn:
|
|
398
|
-
event: Exclude<
|
|
550
|
+
function emitCursorLiveQueuedEvent(
|
|
551
|
+
turn: CursorLiveTurnState,
|
|
552
|
+
event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
|
|
553
|
+
run?: CursorLiveRun,
|
|
399
554
|
): void {
|
|
400
555
|
if (event.type === "thinking-delta") {
|
|
401
556
|
emitCursorNativeThinkingDelta(turn, event.text);
|
|
402
557
|
} else if (event.type === "thinking-completed") {
|
|
403
558
|
closeCursorNativeThinkingBlock(turn);
|
|
404
559
|
} else if (event.type === "text-delta") {
|
|
560
|
+
if (run) run.emittedText += event.text;
|
|
405
561
|
emitCursorNativeTextDelta(turn, event.text);
|
|
406
562
|
}
|
|
407
563
|
}
|
|
408
564
|
|
|
409
|
-
function collectCursorNativeToolBatch(run:
|
|
565
|
+
function collectCursorNativeToolBatch(run: CursorLiveRun): CursorNativeToolDisplayItem[] {
|
|
410
566
|
const tools: CursorNativeToolDisplayItem[] = [];
|
|
411
567
|
while (run.pendingEvents[0]?.type === "tool") {
|
|
412
568
|
const event = run.pendingEvents.shift();
|
|
@@ -415,7 +571,41 @@ function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToo
|
|
|
415
571
|
return tools;
|
|
416
572
|
}
|
|
417
573
|
|
|
418
|
-
function
|
|
574
|
+
function collectCursorBridgeToolBatch(run: CursorLiveRun): CursorPiBridgeToolRequest[] {
|
|
575
|
+
const requests: CursorPiBridgeToolRequest[] = [];
|
|
576
|
+
while (run.pendingEvents[0]?.type === "bridge-tool") {
|
|
577
|
+
const event = run.pendingEvents.shift();
|
|
578
|
+
if (event?.type === "bridge-tool") requests.push(event.request);
|
|
579
|
+
}
|
|
580
|
+
return requests;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function trimAlreadyEmittedCursorText(text: string, emittedText: string): string {
|
|
584
|
+
if (!text || !emittedText) return text;
|
|
585
|
+
if (text === emittedText) return "";
|
|
586
|
+
if (text.startsWith(emittedText)) return text.slice(emittedText.length);
|
|
587
|
+
if (emittedText.endsWith(text)) return "";
|
|
588
|
+
if (text.trim() === emittedText.trim()) return "";
|
|
589
|
+
if (emittedText.trim().endsWith(text.trim())) return "";
|
|
590
|
+
return text;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function selectCursorFinalText(
|
|
594
|
+
resultText: unknown,
|
|
595
|
+
textDeltas: readonly string[],
|
|
596
|
+
emittedText: string,
|
|
597
|
+
fallbackText?: string,
|
|
598
|
+
): string {
|
|
599
|
+
const candidates = [typeof resultText === "string" ? resultText : undefined, fallbackText, textDeltas.join("")];
|
|
600
|
+
for (const candidate of candidates) {
|
|
601
|
+
if (!hasUsableText(candidate)) continue;
|
|
602
|
+
const trimmedCandidate = trimAlreadyEmittedCursorText(candidate, emittedText);
|
|
603
|
+
if (hasUsableText(trimmedCandidate)) return trimmedCandidate;
|
|
604
|
+
}
|
|
605
|
+
return "";
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function takeCursorNativePromptInputTokens(run: CursorLiveRun): number {
|
|
419
609
|
// Native replay can split one Cursor run into multiple pi turns; count prompt input once.
|
|
420
610
|
if (run.promptInputTokensReported) return 0;
|
|
421
611
|
run.promptInputTokensReported = true;
|
|
@@ -425,7 +615,7 @@ function takeCursorNativePromptInputTokens(run: CursorNativeLiveRun): number {
|
|
|
425
615
|
function emitCursorNativeToolUseTurn(
|
|
426
616
|
stream: AssistantMessageEventStream,
|
|
427
617
|
partial: AssistantMessage,
|
|
428
|
-
run:
|
|
618
|
+
run: CursorLiveRun,
|
|
429
619
|
tools: CursorNativeToolDisplayItem[],
|
|
430
620
|
outputText: string,
|
|
431
621
|
): void {
|
|
@@ -452,14 +642,46 @@ function emitCursorNativeToolUseTurn(
|
|
|
452
642
|
scheduleCursorNativeRunIdleDispose(run);
|
|
453
643
|
}
|
|
454
644
|
|
|
455
|
-
|
|
645
|
+
function emitCursorBridgeToolUseTurn(
|
|
646
|
+
stream: AssistantMessageEventStream,
|
|
647
|
+
partial: AssistantMessage,
|
|
648
|
+
run: CursorLiveRun,
|
|
649
|
+
requests: CursorPiBridgeToolRequest[],
|
|
650
|
+
outputText: string,
|
|
651
|
+
): void {
|
|
652
|
+
for (const request of requests) {
|
|
653
|
+
const contentIndex = partial.content.length;
|
|
654
|
+
partial.content.push({
|
|
655
|
+
type: "toolCall",
|
|
656
|
+
id: request.piToolCallId,
|
|
657
|
+
name: request.piToolName,
|
|
658
|
+
arguments: request.args,
|
|
659
|
+
});
|
|
660
|
+
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
661
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(request.args), partial });
|
|
662
|
+
const block = partial.content[contentIndex];
|
|
663
|
+
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
664
|
+
}
|
|
665
|
+
setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
|
|
666
|
+
partial.stopReason = "toolUse";
|
|
667
|
+
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
668
|
+
scheduleCursorNativeRunIdleDispose(run);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function disposeCursorNativeRun(run: CursorLiveRun): Promise<void> {
|
|
456
672
|
if (run.disposed) return;
|
|
457
673
|
run.disposed = true;
|
|
458
|
-
|
|
674
|
+
pendingCursorLiveRuns.delete(run.id);
|
|
459
675
|
clearCursorNativeRunIdleDispose(run);
|
|
676
|
+
run.bridgeRun?.cancel("Cursor live run disposed");
|
|
460
677
|
for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
|
|
461
678
|
run.recordedToolDisplayIds = [];
|
|
462
679
|
run.waiters.clear();
|
|
680
|
+
try {
|
|
681
|
+
await run.bridgeRun?.dispose();
|
|
682
|
+
} catch {
|
|
683
|
+
// bridge disposal failure should not mask the provider result
|
|
684
|
+
}
|
|
463
685
|
try {
|
|
464
686
|
await run.agent[Symbol.asyncDispose]();
|
|
465
687
|
} catch {
|
|
@@ -470,10 +692,10 @@ async function disposeCursorNativeRun(run: CursorNativeLiveRun): Promise<void> {
|
|
|
470
692
|
async function emitCursorNativeRunNextTurn(
|
|
471
693
|
stream: AssistantMessageEventStream,
|
|
472
694
|
partial: AssistantMessage,
|
|
473
|
-
run:
|
|
695
|
+
run: CursorLiveRun,
|
|
474
696
|
signal?: AbortSignal,
|
|
475
697
|
): Promise<void> {
|
|
476
|
-
const turn:
|
|
698
|
+
const turn: CursorLiveTurnState = {
|
|
477
699
|
stream,
|
|
478
700
|
partial,
|
|
479
701
|
thinkingContentIndex: -1,
|
|
@@ -484,14 +706,23 @@ async function emitCursorNativeRunNextTurn(
|
|
|
484
706
|
while (run.pendingEvents.length > 0) {
|
|
485
707
|
const event = run.pendingEvents[0];
|
|
486
708
|
if (event.type === "tool") {
|
|
487
|
-
await
|
|
709
|
+
await settleCursorLiveToolBatch(run);
|
|
710
|
+
if (signal?.aborted) throw new CursorAbortError();
|
|
488
711
|
const outputText = closeCursorNativeTurnBlocks(turn);
|
|
489
712
|
const tools = collectCursorNativeToolBatch(run);
|
|
490
713
|
emitCursorNativeToolUseTurn(stream, partial, run, tools, outputText);
|
|
491
714
|
return;
|
|
492
715
|
}
|
|
716
|
+
if (event.type === "bridge-tool") {
|
|
717
|
+
await settleCursorLiveToolBatch(run);
|
|
718
|
+
if (signal?.aborted) throw new CursorAbortError();
|
|
719
|
+
const outputText = closeCursorNativeTurnBlocks(turn);
|
|
720
|
+
const requests = collectCursorBridgeToolBatch(run);
|
|
721
|
+
emitCursorBridgeToolUseTurn(stream, partial, run, requests, outputText);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
493
724
|
run.pendingEvents.shift();
|
|
494
|
-
|
|
725
|
+
emitCursorLiveQueuedEvent(turn, event, run);
|
|
495
726
|
}
|
|
496
727
|
|
|
497
728
|
if (run.cancelled) {
|
|
@@ -509,8 +740,9 @@ async function emitCursorNativeRunNextTurn(
|
|
|
509
740
|
}
|
|
510
741
|
if (run.done) {
|
|
511
742
|
let outputText = closeCursorNativeTurnBlocks(turn);
|
|
512
|
-
|
|
513
|
-
|
|
743
|
+
const finalText = trimAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), run.emittedText);
|
|
744
|
+
if (finalText) {
|
|
745
|
+
outputText += await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
|
|
514
746
|
}
|
|
515
747
|
setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
|
|
516
748
|
partial.stopReason = "stop";
|
|
@@ -523,17 +755,16 @@ async function emitCursorNativeRunNextTurn(
|
|
|
523
755
|
}
|
|
524
756
|
}
|
|
525
757
|
|
|
526
|
-
async function
|
|
758
|
+
async function replayPendingCursorLiveRun(
|
|
527
759
|
stream: AssistantMessageEventStream,
|
|
528
760
|
partial: AssistantMessage,
|
|
529
761
|
context: Context,
|
|
530
762
|
signal?: AbortSignal,
|
|
531
763
|
): Promise<boolean> {
|
|
532
|
-
const
|
|
533
|
-
if (!replayId) return false;
|
|
534
|
-
const run = pendingCursorNativeRuns.get(replayId);
|
|
764
|
+
const run = getPendingCursorLiveRun(context);
|
|
535
765
|
if (!run) return false;
|
|
536
766
|
clearCursorNativeRunIdleDispose(run);
|
|
767
|
+
run.bridgeRun?.resolveToolResultsFromContext(context);
|
|
537
768
|
try {
|
|
538
769
|
await emitCursorNativeRunNextTurn(stream, partial, run, signal);
|
|
539
770
|
} catch (error) {
|
|
@@ -553,10 +784,15 @@ export function streamCursor(
|
|
|
553
784
|
(async () => {
|
|
554
785
|
const partial = makeInitialMessage(model);
|
|
555
786
|
let agent: SDKAgent | null = null;
|
|
556
|
-
let
|
|
787
|
+
let activeLiveRun: CursorLiveRun | undefined;
|
|
788
|
+
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
789
|
+
let bridgeRunOwnedByLiveRun = false;
|
|
790
|
+
let liveRunForBridgeQueue: CursorLiveRun | undefined;
|
|
791
|
+
const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
|
|
557
792
|
let resolvedApiKey: string | undefined;
|
|
558
793
|
let abortSignal: AbortSignal | undefined;
|
|
559
794
|
let abortListener: (() => void) | undefined;
|
|
795
|
+
let restoreCursorSdkOutputFilter: (() => void) | undefined;
|
|
560
796
|
|
|
561
797
|
try {
|
|
562
798
|
const throwIfAborted = (): void => {
|
|
@@ -566,7 +802,7 @@ export function streamCursor(
|
|
|
566
802
|
stream.push({ type: "start", partial });
|
|
567
803
|
throwIfAborted();
|
|
568
804
|
|
|
569
|
-
if (await
|
|
805
|
+
if (await replayPendingCursorLiveRun(stream, partial, context, options?.signal)) {
|
|
570
806
|
stream.end();
|
|
571
807
|
return;
|
|
572
808
|
}
|
|
@@ -575,18 +811,40 @@ export function streamCursor(
|
|
|
575
811
|
if (!apiKey) throw new Error(MISSING_API_KEY_MESSAGE);
|
|
576
812
|
resolvedApiKey = apiKey;
|
|
577
813
|
|
|
578
|
-
// pi-ai Context/SimpleStreamOptions do not
|
|
579
|
-
//
|
|
580
|
-
const cwd =
|
|
814
|
+
// pi-ai Context/SimpleStreamOptions do not expose ExtensionContext.cwd; bridge via session_start
|
|
815
|
+
// until pi threads session cwd into streamSimple (cwd can change without a new session event).
|
|
816
|
+
const cwd = getCursorSessionCwd();
|
|
581
817
|
const fastEnabled = getEffectiveFastForModelId(model.id);
|
|
582
818
|
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
583
819
|
const settingSources = resolveCursorSettingSources();
|
|
584
820
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
821
|
+
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
822
|
+
bridgeRun = registeredBridge
|
|
823
|
+
? await registeredBridge.createRun({
|
|
824
|
+
onToolRequest: (request) => {
|
|
825
|
+
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
826
|
+
queueCursorNativeEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
827
|
+
} else {
|
|
828
|
+
queuedBridgeRequestsBeforeLiveRun.push(request);
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
})
|
|
832
|
+
: undefined;
|
|
833
|
+
if (!bridgeRun?.enabled || !bridgeRun.mcpServers) {
|
|
834
|
+
await bridgeRun?.dispose();
|
|
835
|
+
bridgeRun = undefined;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
installCursorMcpToolTimeoutOverride();
|
|
839
|
+
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
840
|
+
agent = await suppressCursorSdkOutput(() =>
|
|
841
|
+
Agent.create({
|
|
842
|
+
apiKey,
|
|
843
|
+
model: selection,
|
|
844
|
+
local: settingSources ? { cwd, settingSources } : { cwd },
|
|
845
|
+
...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
|
|
846
|
+
}),
|
|
847
|
+
);
|
|
590
848
|
throwIfAborted();
|
|
591
849
|
|
|
592
850
|
const prompt = buildCursorPrompt(context, {
|
|
@@ -604,14 +862,17 @@ export function streamCursor(
|
|
|
604
862
|
const nativeReplayId = createCursorNativeReplayId();
|
|
605
863
|
const textDeltas: string[] = [];
|
|
606
864
|
let nativeToolReplayStarted = false;
|
|
607
|
-
const
|
|
865
|
+
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
866
|
+
const liveRun: CursorLiveRun | undefined = useLiveRun
|
|
608
867
|
? {
|
|
609
|
-
id: nativeReplayId,
|
|
868
|
+
id: useNativeToolReplay ? nativeReplayId : bridgeRun!.id,
|
|
610
869
|
agent,
|
|
870
|
+
bridgeRun,
|
|
611
871
|
promptInputTokens,
|
|
612
872
|
promptInputTokensReported: false,
|
|
613
873
|
pendingEvents: [],
|
|
614
874
|
textDeltas,
|
|
875
|
+
emittedText: "",
|
|
615
876
|
recordedToolDisplayIds: [],
|
|
616
877
|
done: false,
|
|
617
878
|
cancelled: false,
|
|
@@ -620,11 +881,21 @@ export function streamCursor(
|
|
|
620
881
|
}
|
|
621
882
|
: undefined;
|
|
622
883
|
if (liveRun) {
|
|
623
|
-
|
|
624
|
-
|
|
884
|
+
pendingCursorLiveRuns.set(liveRun.id, liveRun);
|
|
885
|
+
activeLiveRun = liveRun;
|
|
886
|
+
liveRunForBridgeQueue = liveRun;
|
|
887
|
+
bridgeRunOwnedByLiveRun = bridgeRun !== undefined;
|
|
888
|
+
for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
|
|
889
|
+
queueCursorNativeEvent(liveRun, { type: "bridge-tool", request });
|
|
890
|
+
}
|
|
625
891
|
}
|
|
626
892
|
const startedToolCalls = new Map<string, unknown>();
|
|
893
|
+
const bridgeStartedToolCallIds = new Set<string>();
|
|
894
|
+
const activeShellCallIds = new Set<string>();
|
|
895
|
+
const ambiguousShellOutputCallIds = new Set<string>();
|
|
896
|
+
const shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
|
|
627
897
|
const completedToolIdentities = new Set<string>();
|
|
898
|
+
let cursorPlanTextCandidate: string | undefined;
|
|
628
899
|
const completedStartedToolFingerprints = new Set<string>();
|
|
629
900
|
const completedFallbackToolFingerprints = new Set<string>();
|
|
630
901
|
|
|
@@ -684,6 +955,16 @@ export function streamCursor(
|
|
|
684
955
|
closeTraceBlock();
|
|
685
956
|
};
|
|
686
957
|
|
|
958
|
+
const emitCursorToolTrace = (text: string): void => {
|
|
959
|
+
const traceText = text.endsWith("\n") ? text : `${text}\n`;
|
|
960
|
+
if (liveRun) {
|
|
961
|
+
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: traceText });
|
|
962
|
+
queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
appendTraceBlock(traceText);
|
|
966
|
+
};
|
|
967
|
+
|
|
687
968
|
const closeTraceBlock = (): void => {
|
|
688
969
|
if (thinkingContentIndex < 0) return;
|
|
689
970
|
const block = partial.content[thinkingContentIndex];
|
|
@@ -720,10 +1001,80 @@ export function streamCursor(
|
|
|
720
1001
|
}
|
|
721
1002
|
};
|
|
722
1003
|
|
|
1004
|
+
const getStartedToolCallFingerprint = (toolCall: unknown): string => {
|
|
1005
|
+
return getToolFingerprint({ toolName: getCursorToolName(toolCall), args: getObjectField(toolCall, "args") });
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
const clearStartedToolCall = (callId: string): void => {
|
|
1009
|
+
startedToolCalls.delete(callId);
|
|
1010
|
+
bridgeStartedToolCallIds.delete(callId);
|
|
1011
|
+
activeShellCallIds.delete(callId);
|
|
1012
|
+
ambiguousShellOutputCallIds.delete(callId);
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const takeBridgeStartedToolCallId = (callId: unknown): string | undefined => {
|
|
1016
|
+
if (typeof callId !== "string" || !bridgeStartedToolCallIds.has(callId)) return undefined;
|
|
1017
|
+
bridgeStartedToolCallIds.delete(callId);
|
|
1018
|
+
return callId;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const takeShellOutputDeltas = (callId: string): CursorShellOutputDeltas | undefined => {
|
|
1022
|
+
const deltas = shellOutputDeltasByCallId.get(callId);
|
|
1023
|
+
shellOutputDeltasByCallId.delete(callId);
|
|
1024
|
+
return deltas;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const appendShellOutputDelta = (delta: CursorShellOutputDelta): void => {
|
|
1028
|
+
if (activeShellCallIds.size !== 1) {
|
|
1029
|
+
for (const activeCallId of activeShellCallIds) {
|
|
1030
|
+
ambiguousShellOutputCallIds.add(activeCallId);
|
|
1031
|
+
shellOutputDeltasByCallId.delete(activeCallId);
|
|
1032
|
+
}
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const [callId] = activeShellCallIds;
|
|
1036
|
+
if (!callId || ambiguousShellOutputCallIds.has(callId)) return;
|
|
1037
|
+
let deltas = shellOutputDeltasByCallId.get(callId);
|
|
1038
|
+
if (!deltas) {
|
|
1039
|
+
deltas = { stdout: [], stderr: [] };
|
|
1040
|
+
shellOutputDeltasByCallId.set(callId, deltas);
|
|
1041
|
+
}
|
|
1042
|
+
deltas[delta.stream].push(delta.data);
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const removeStartedToolCallForStep = (toolCall: unknown, stepId: unknown): string | undefined => {
|
|
1046
|
+
if (typeof stepId === "string" && startedToolCalls.has(stepId)) {
|
|
1047
|
+
clearStartedToolCall(stepId);
|
|
1048
|
+
return stepId;
|
|
1049
|
+
}
|
|
1050
|
+
const fingerprint = getStartedToolCallFingerprint(toolCall);
|
|
1051
|
+
for (const [callId, startedToolCall] of startedToolCalls) {
|
|
1052
|
+
if (getStartedToolCallFingerprint(startedToolCall) !== fingerprint) continue;
|
|
1053
|
+
clearStartedToolCall(callId);
|
|
1054
|
+
return callId;
|
|
1055
|
+
}
|
|
1056
|
+
return undefined;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const discardIncompleteStartedToolCalls = (): void => {
|
|
1060
|
+
startedToolCalls.clear();
|
|
1061
|
+
bridgeStartedToolCallIds.clear();
|
|
1062
|
+
activeShellCallIds.clear();
|
|
1063
|
+
ambiguousShellOutputCallIds.clear();
|
|
1064
|
+
shellOutputDeltasByCallId.clear();
|
|
1065
|
+
};
|
|
1066
|
+
|
|
723
1067
|
const handleCompletedToolCall = (
|
|
724
1068
|
toolCall: unknown,
|
|
725
1069
|
options: { identity?: string; source?: "started" | "fallback" } = {},
|
|
726
1070
|
): void => {
|
|
1071
|
+
const planText = getCursorCreatePlanText(toolCall);
|
|
1072
|
+
if (planText) cursorPlanTextCandidate = scrubSensitiveText(planText, resolvedApiKey);
|
|
1073
|
+
|
|
1074
|
+
if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
|
|
1075
|
+
if (options.identity) completedToolIdentities.add(options.identity);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
727
1078
|
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
|
|
728
1079
|
const display = buildCursorPiToolDisplay(toolCall, { cwd });
|
|
729
1080
|
const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
|
|
@@ -740,11 +1091,10 @@ export function streamCursor(
|
|
|
740
1091
|
completedFallbackToolFingerprints.add(fingerprint);
|
|
741
1092
|
}
|
|
742
1093
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
1094
|
+
const nativeRenderable = canRenderCursorToolNatively(display.toolName);
|
|
1095
|
+
const route = useNativeToolReplay && nativeRenderable && liveRun ? "native_replay" : "trace";
|
|
1096
|
+
|
|
1097
|
+
if (route === "native_replay" && liveRun) {
|
|
748
1098
|
nativeToolReplayStarted = true;
|
|
749
1099
|
const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
|
|
750
1100
|
queueCursorNativeEvent(liveRun, {
|
|
@@ -759,7 +1109,7 @@ export function streamCursor(
|
|
|
759
1109
|
return;
|
|
760
1110
|
}
|
|
761
1111
|
|
|
762
|
-
|
|
1112
|
+
emitCursorToolTrace(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
|
|
763
1113
|
};
|
|
764
1114
|
|
|
765
1115
|
const onDelta = (args: { update: InteractionUpdate }): void => {
|
|
@@ -767,36 +1117,50 @@ export function streamCursor(
|
|
|
767
1117
|
|
|
768
1118
|
if (update.type === "text-delta") {
|
|
769
1119
|
textDeltas.push(update.text);
|
|
770
|
-
if (liveRun
|
|
1120
|
+
if (liveRun) {
|
|
771
1121
|
queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
|
|
772
|
-
} else
|
|
1122
|
+
} else {
|
|
773
1123
|
appendLiveTextDelta(update.text);
|
|
774
1124
|
}
|
|
775
1125
|
} else if (update.type === "thinking-delta") {
|
|
776
|
-
if (liveRun
|
|
1126
|
+
if (liveRun) {
|
|
777
1127
|
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
|
|
778
1128
|
} else {
|
|
779
1129
|
appendTraceDelta(update.text);
|
|
780
1130
|
}
|
|
781
1131
|
} else if (update.type === "thinking-completed") {
|
|
782
|
-
if (liveRun
|
|
1132
|
+
if (liveRun) {
|
|
783
1133
|
queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
|
|
784
1134
|
} else {
|
|
785
1135
|
closeTraceBlock();
|
|
786
1136
|
}
|
|
787
1137
|
} else if (update.type === "tool-call-started") {
|
|
788
|
-
|
|
1138
|
+
if (liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
|
|
1139
|
+
if (typeof update.callId === "string") bridgeStartedToolCallIds.add(update.callId);
|
|
1140
|
+
} else {
|
|
1141
|
+
startedToolCalls.set(update.callId, update.toolCall);
|
|
1142
|
+
if (isCursorShellToolCall(update.toolCall)) activeShellCallIds.add(update.callId);
|
|
1143
|
+
}
|
|
789
1144
|
} else if (update.type === "tool-call-completed") {
|
|
790
|
-
const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
|
|
791
|
-
startedToolCalls.delete(update.callId);
|
|
792
1145
|
const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
|
|
793
|
-
|
|
1146
|
+
const bridgeStartedCallId = takeBridgeStartedToolCallId(update.callId);
|
|
1147
|
+
if (bridgeStartedCallId) {
|
|
1148
|
+
completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
|
|
1152
|
+
clearStartedToolCall(update.callId);
|
|
1153
|
+
const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(mergedToolCall, takeShellOutputDeltas(update.callId));
|
|
1154
|
+
handleCompletedToolCall(toolCallWithShellOutput, {
|
|
794
1155
|
identity,
|
|
795
1156
|
source: identity ? "started" : "fallback",
|
|
796
1157
|
});
|
|
1158
|
+
} else if (update.type === "shell-output-delta") {
|
|
1159
|
+
const delta = getCursorShellOutputDelta(update);
|
|
1160
|
+
if (delta) appendShellOutputDelta(delta);
|
|
797
1161
|
} else if (update.type === "summary") {
|
|
798
1162
|
const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
|
|
799
|
-
if (liveRun
|
|
1163
|
+
if (liveRun) {
|
|
800
1164
|
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
|
|
801
1165
|
} else {
|
|
802
1166
|
appendTraceDelta(summary);
|
|
@@ -805,24 +1169,43 @@ export function streamCursor(
|
|
|
805
1169
|
// Cursor turn-ended usage is intentionally not copied into pi usage: the SDK reports
|
|
806
1170
|
// cumulative internal agent/tool/cache tokens, not the replayable pi prompt context.
|
|
807
1171
|
// partial-tool-call, summary-started, summary-completed, turn-ended,
|
|
808
|
-
//
|
|
1172
|
+
// token-delta, step-* are intentionally not surfaced.
|
|
809
1173
|
};
|
|
810
1174
|
|
|
811
1175
|
const onStep = (args: { step: unknown }): void => {
|
|
1176
|
+
const stepType = getObjectField(args.step, "type");
|
|
812
1177
|
const step = getObjectField(args.step, "message") ? args.step : undefined;
|
|
813
|
-
|
|
814
|
-
|
|
1178
|
+
const rawStepToolCall = getObjectField(step, "message");
|
|
1179
|
+
if (stepType !== "toolCall") return;
|
|
1180
|
+
const toolCall = rawStepToolCall;
|
|
815
1181
|
const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
|
|
816
1182
|
if (toolCall) {
|
|
817
|
-
|
|
818
|
-
|
|
1183
|
+
const bridgeStartedCallId = takeBridgeStartedToolCallId(stepId);
|
|
1184
|
+
if (bridgeStartedCallId) {
|
|
1185
|
+
completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const matchedStartedCallId = removeStartedToolCallForStep(toolCall, stepId);
|
|
1189
|
+
const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(
|
|
1190
|
+
toolCall,
|
|
1191
|
+
matchedStartedCallId ? takeShellOutputDeltas(matchedStartedCallId) : undefined,
|
|
1192
|
+
);
|
|
1193
|
+
if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
|
|
1194
|
+
if (matchedStartedCallId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const identityId = typeof stepId === "string" ? stepId : matchedStartedCallId;
|
|
1198
|
+
handleCompletedToolCall(toolCallWithShellOutput, {
|
|
1199
|
+
identity: identityId ? `cursor-tool:${identityId}` : undefined,
|
|
819
1200
|
});
|
|
1201
|
+
if (matchedStartedCallId && matchedStartedCallId !== stepId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
|
|
820
1202
|
}
|
|
821
1203
|
};
|
|
822
1204
|
|
|
823
1205
|
// Handle abort signal
|
|
824
1206
|
let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
|
|
825
1207
|
abortListener = () => {
|
|
1208
|
+
activeLiveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
826
1209
|
if (run) {
|
|
827
1210
|
run.cancel().catch(() => {});
|
|
828
1211
|
}
|
|
@@ -840,15 +1223,16 @@ export function streamCursor(
|
|
|
840
1223
|
throw new CursorAbortError();
|
|
841
1224
|
}
|
|
842
1225
|
|
|
843
|
-
if (
|
|
1226
|
+
if (liveRun) {
|
|
844
1227
|
void run
|
|
845
1228
|
.wait()
|
|
846
1229
|
.then(async (result) => {
|
|
847
1230
|
if (liveRun.disposed) return;
|
|
1231
|
+
discardIncompleteStartedToolCalls();
|
|
848
1232
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
849
1233
|
if (liveRun.disposed) return;
|
|
850
1234
|
liveRun.cancelled = result.status === "cancelled";
|
|
851
|
-
liveRun.finalText =
|
|
1235
|
+
liveRun.finalText = selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, cursorPlanTextCandidate);
|
|
852
1236
|
liveRun.done = true;
|
|
853
1237
|
notifyCursorNativeRun(liveRun);
|
|
854
1238
|
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
@@ -862,7 +1246,7 @@ export function streamCursor(
|
|
|
862
1246
|
|
|
863
1247
|
try {
|
|
864
1248
|
await waitForCursorNativeRunProgress(liveRun, options?.signal);
|
|
865
|
-
await
|
|
1249
|
+
await settleCursorLiveToolBatch(liveRun);
|
|
866
1250
|
closeTraceBlock();
|
|
867
1251
|
await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
|
|
868
1252
|
} catch (error) {
|
|
@@ -874,6 +1258,7 @@ export function streamCursor(
|
|
|
874
1258
|
}
|
|
875
1259
|
|
|
876
1260
|
const result = await run.wait();
|
|
1261
|
+
discardIncompleteStartedToolCalls();
|
|
877
1262
|
await cacheSdkContextWindow(agent.agentId, model.id);
|
|
878
1263
|
|
|
879
1264
|
// Close any open thinking/activity trace, then use the final run result only when
|
|
@@ -884,11 +1269,13 @@ export function streamCursor(
|
|
|
884
1269
|
partial.stopReason = "aborted";
|
|
885
1270
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
886
1271
|
} else {
|
|
887
|
-
const
|
|
1272
|
+
const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""), cursorPlanTextCandidate);
|
|
1273
|
+
const finalText = flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
|
|
888
1274
|
setApproximateUsage(partial, promptInputTokens, finalText);
|
|
889
1275
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
890
1276
|
}
|
|
891
1277
|
} catch (error) {
|
|
1278
|
+
if (activeLiveRun && !activeLiveRun.disposed) await disposeCursorNativeRun(activeLiveRun);
|
|
892
1279
|
if (error instanceof CursorAbortError) {
|
|
893
1280
|
partial.stopReason = "aborted";
|
|
894
1281
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
@@ -898,12 +1285,21 @@ export function streamCursor(
|
|
|
898
1285
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
899
1286
|
}
|
|
900
1287
|
} finally {
|
|
901
|
-
|
|
1288
|
+
restoreCursorSdkOutputFilter?.();
|
|
1289
|
+
if (activeLiveRun?.disposed) agent = null;
|
|
902
1290
|
|
|
903
1291
|
if (abortSignal && abortListener) {
|
|
904
1292
|
abortSignal.removeEventListener("abort", abortListener);
|
|
905
1293
|
}
|
|
906
1294
|
|
|
1295
|
+
if (bridgeRun && !bridgeRunOwnedByLiveRun) {
|
|
1296
|
+
try {
|
|
1297
|
+
await bridgeRun.dispose();
|
|
1298
|
+
} catch {
|
|
1299
|
+
// bridge disposal failure should not mask original error
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
907
1303
|
if (agent) {
|
|
908
1304
|
try {
|
|
909
1305
|
await agent[Symbol.asyncDispose]();
|
|
@@ -922,7 +1318,7 @@ export function streamCursor(
|
|
|
922
1318
|
|
|
923
1319
|
export const __testUtils = {
|
|
924
1320
|
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
925
|
-
pendingCursorNativeRunCount: () =>
|
|
1321
|
+
pendingCursorNativeRunCount: () => pendingCursorLiveRuns.size,
|
|
926
1322
|
setCursorNativeReplayIdleDisposeMs: (value: number) => {
|
|
927
1323
|
cursorNativeReplayIdleDisposeMs = value;
|
|
928
1324
|
},
|