pi-cursor-sdk 0.1.16 → 0.1.18
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 +53 -1
- package/README.md +2 -2
- package/docs/cursor-live-smoke-checklist.md +54 -41
- package/docs/cursor-model-ux-spec.md +4 -3
- package/docs/cursor-testing-lessons.md +199 -0
- package/package.json +14 -5
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/steering-rpc-smoke.mjs +238 -0
- package/scripts/tmux-live-smoke.sh +418 -0
- package/scripts/validate-smoke-jsonl.mjs +207 -0
- package/src/cursor-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-edit-diff.ts +11 -0
- package/src/cursor-env-boolean.ts +22 -0
- package/src/cursor-live-run-coordinator.ts +483 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +103 -0
- package/src/cursor-native-tool-display-replay.ts +465 -0
- package/src/cursor-native-tool-display-state.ts +78 -0
- package/src/cursor-native-tool-display-tools.ts +102 -0
- package/src/cursor-native-tool-display.ts +10 -648
- package/src/cursor-partial-content-emitter.ts +121 -0
- package/src/cursor-pi-tool-bridge-abort.ts +133 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +179 -0
- package/src/cursor-pi-tool-bridge-mcp.ts +118 -0
- package/src/cursor-pi-tool-bridge-run.ts +384 -0
- package/src/cursor-pi-tool-bridge-server.ts +182 -0
- package/src/cursor-pi-tool-bridge-snapshot.ts +88 -0
- package/src/cursor-pi-tool-bridge-types.ts +80 -0
- package/src/cursor-pi-tool-bridge.ts +42 -1104
- package/src/cursor-provider-live-run-drain.ts +405 -0
- package/src/cursor-provider-turn-coordinator.ts +460 -0
- package/src/cursor-provider.ts +77 -1103
- package/src/cursor-question-tool.ts +9 -1
- package/src/cursor-record-utils.ts +26 -0
- package/src/cursor-sdk-output-filter.ts +100 -0
- package/src/cursor-sensitive-text.ts +37 -0
- package/src/cursor-tool-transcript.ts +28 -1229
- package/src/cursor-transcript-tool-formatters.ts +641 -0
- package/src/cursor-transcript-tool-specs.ts +441 -0
- package/src/cursor-transcript-utils.ts +276 -0
package/src/cursor-provider.ts
CHANGED
|
@@ -6,12 +6,11 @@ import {
|
|
|
6
6
|
type Model,
|
|
7
7
|
type SimpleStreamOptions,
|
|
8
8
|
type AssistantMessage,
|
|
9
|
-
type ToolResultMessage,
|
|
10
9
|
} from "@earendil-works/pi-ai";
|
|
11
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
10
|
import { Agent, createAgentPlatform } from "@cursor/sdk";
|
|
13
|
-
import type {
|
|
11
|
+
import type { SDKAgent, SettingSource } from "@cursor/sdk";
|
|
14
12
|
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
13
|
+
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
|
|
15
14
|
import { buildCursorSendPrompt } from "./context.js";
|
|
16
15
|
import {
|
|
17
16
|
acquireSessionCursorAgent,
|
|
@@ -23,29 +22,34 @@ import {
|
|
|
23
22
|
type CursorPiBridgeToolRequest,
|
|
24
23
|
type CursorPiToolBridgeRun,
|
|
25
24
|
} 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
25
|
import {
|
|
33
26
|
applyCursorApproximateUsage,
|
|
34
27
|
estimateCursorPromptInputTokens,
|
|
35
28
|
getCursorPromptOptions,
|
|
36
29
|
} from "./cursor-usage-accounting.js";
|
|
37
30
|
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
31
|
+
import { getActiveContextToolNames } from "./cursor-context-tools.js";
|
|
32
|
+
import { CursorLiveRunAbortError, type CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
33
|
+
import {
|
|
34
|
+
abandonSessionCursorAgent,
|
|
35
|
+
createCursorNativeReplayId,
|
|
36
|
+
cursorLiveRuns,
|
|
37
|
+
drainCursorLiveRunTurn,
|
|
38
|
+
drainExistingCursorLiveRunBeforeSend,
|
|
39
|
+
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
40
|
+
getPendingCursorLiveRun,
|
|
41
|
+
hasTrailingUserMessagesAfterToolResults,
|
|
42
|
+
releaseAllPendingCursorLiveRunsForTests,
|
|
43
|
+
resetCursorNativeReplayIdleDisposeMs,
|
|
44
|
+
selectCursorFinalText,
|
|
45
|
+
setCursorNativeReplayIdleDisposeMs,
|
|
46
|
+
settleCursorLiveToolBatch,
|
|
47
|
+
} from "./cursor-provider-live-run-drain.js";
|
|
38
48
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
39
49
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
40
50
|
import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
canRenderCursorToolNatively,
|
|
44
|
-
isCursorNativeToolDisplayRuntimeEnabled,
|
|
45
|
-
deleteCursorNativeToolDisplay,
|
|
46
|
-
recordCursorNativeToolDisplay,
|
|
47
|
-
type CursorNativeToolDisplayItem,
|
|
48
|
-
} from "./cursor-native-tool-display.js";
|
|
51
|
+
import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
52
|
+
import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display.js";
|
|
49
53
|
|
|
50
54
|
function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
51
55
|
return {
|
|
@@ -67,13 +71,6 @@ function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
class CursorAbortError extends Error {
|
|
71
|
-
constructor() {
|
|
72
|
-
super("aborted");
|
|
73
|
-
this.name = "CursorAbortError";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
74
|
const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
78
75
|
const MISSING_API_KEY_MESSAGE =
|
|
79
76
|
"Cursor SDK runs require a Cursor API key. Run /login -> Use an API key -> Cursor, set CURSOR_API_KEY before starting pi, or restart pi with --api-key.";
|
|
@@ -81,74 +78,10 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
81
78
|
"Cursor SDK request failed. The API key may be missing, invalid, or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
82
79
|
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
83
80
|
"Cursor SDK request failed because the API key may be invalid or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
84
|
-
const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
|
|
85
|
-
const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
86
|
-
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
87
81
|
const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
88
|
-
const cursorSdkOutputSuppression = new AsyncLocalStorage<boolean>();
|
|
89
|
-
|
|
90
|
-
type CursorLiveQueuedEvent =
|
|
91
|
-
| { type: "thinking-delta"; text: string }
|
|
92
|
-
| { type: "thinking-completed" }
|
|
93
|
-
| { type: "text-delta"; text: string }
|
|
94
|
-
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
95
|
-
| { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
|
|
96
|
-
|
|
97
|
-
interface CursorLiveSdkRun {
|
|
98
|
-
cancel(): Promise<void>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface CursorLiveRun {
|
|
102
|
-
id: string;
|
|
103
|
-
agent: SDKAgent;
|
|
104
|
-
bridgeRun?: CursorPiToolBridgeRun;
|
|
105
|
-
sessionBridgeRun?: CursorPiToolBridgeRun;
|
|
106
|
-
sessionAgentScopeKey?: string;
|
|
107
|
-
sdkRun?: CursorLiveSdkRun;
|
|
108
|
-
accounting: CursorLiveRunAccountingState;
|
|
109
|
-
pendingEvents: CursorLiveQueuedEvent[];
|
|
110
|
-
textDeltas: string[];
|
|
111
|
-
emittedText: string;
|
|
112
|
-
recordedToolDisplayIds: string[];
|
|
113
|
-
finalText?: string;
|
|
114
|
-
done: boolean;
|
|
115
|
-
cancelled: boolean;
|
|
116
|
-
disposed: boolean;
|
|
117
|
-
errorMessage?: string;
|
|
118
|
-
idleDisposeTimer?: ReturnType<typeof setTimeout>;
|
|
119
|
-
waiters: Set<() => void>;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface CursorLiveTurnState {
|
|
123
|
-
stream: AssistantMessageEventStream;
|
|
124
|
-
partial: AssistantMessage;
|
|
125
|
-
thinkingContentIndex: number;
|
|
126
|
-
textContentIndex: number;
|
|
127
|
-
emittedText: string;
|
|
128
|
-
}
|
|
129
82
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const pendingCursorLiveRuns = new Map<string, CursorLiveRun>();
|
|
133
|
-
|
|
134
|
-
function escapeRegExp(value: string): string {
|
|
135
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function scrubSensitiveText(text: string, apiKey?: string): string {
|
|
139
|
-
let scrubbed = text;
|
|
140
|
-
const trimmedKey = apiKey?.trim();
|
|
141
|
-
if (trimmedKey) {
|
|
142
|
-
scrubbed = scrubbed.replace(new RegExp(escapeRegExp(trimmedKey), "g"), "[redacted]");
|
|
143
|
-
}
|
|
144
|
-
return scrubbed
|
|
145
|
-
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
146
|
-
.replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
|
|
147
|
-
.replace(
|
|
148
|
-
/((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
|
|
149
|
-
"$1[redacted]",
|
|
150
|
-
);
|
|
151
|
-
}
|
|
83
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
84
|
+
import { hasUsableText } from "./cursor-record-utils.js";
|
|
152
85
|
|
|
153
86
|
function isGenericErrorMessage(message: string): boolean {
|
|
154
87
|
const normalized = message.trim().toLowerCase();
|
|
@@ -187,92 +120,6 @@ function sanitizeError(error: unknown, apiKey?: string): string {
|
|
|
187
120
|
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
188
121
|
}
|
|
189
122
|
|
|
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
|
-
|
|
262
|
-
function getObjectField(value: unknown, field: string): unknown {
|
|
263
|
-
if (!value || typeof value !== "object") return undefined;
|
|
264
|
-
return (value as Record<string, unknown>)[field];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function getCursorToolName(toolCall: unknown): string {
|
|
268
|
-
if (!toolCall || typeof toolCall !== "object") return "unknown";
|
|
269
|
-
const data = toolCall as Record<string, unknown>;
|
|
270
|
-
if (typeof data.name === "string") return data.name;
|
|
271
|
-
if (typeof data.type === "string") return data.type;
|
|
272
|
-
if (typeof data.toolName === "string") return data.toolName;
|
|
273
|
-
return "unknown";
|
|
274
|
-
}
|
|
275
|
-
|
|
276
123
|
async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<void> {
|
|
277
124
|
try {
|
|
278
125
|
const platform = await createAgentPlatform();
|
|
@@ -284,564 +131,6 @@ async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<
|
|
|
284
131
|
}
|
|
285
132
|
}
|
|
286
133
|
|
|
287
|
-
function sanitizeSingleLine(value: string): string {
|
|
288
|
-
return value.replace(/\s+/g, " ").trim();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function truncateSingleLine(value: string, maxLength = 240): string {
|
|
292
|
-
const sanitized = sanitizeSingleLine(value);
|
|
293
|
-
return sanitized.length > maxLength ? `${sanitized.slice(0, maxLength - 1)}…` : sanitized;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function formatCursorToolName(toolCall: unknown): string {
|
|
297
|
-
return truncateSingleLine(getCursorToolName(toolCall), 80) || "unknown";
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function hasUsableText(value: string | undefined): value is string {
|
|
301
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
302
|
-
}
|
|
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
|
-
|
|
361
|
-
function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
|
|
362
|
-
if (typeof value === "string") return scrubSensitiveText(value, apiKey);
|
|
363
|
-
if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
|
|
364
|
-
if (value && typeof value === "object") {
|
|
365
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, scrubDisplayValue(entry, apiKey)]));
|
|
366
|
-
}
|
|
367
|
-
return value;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function createCursorNativeReplayId(): string {
|
|
371
|
-
cursorNativeReplayCounter += 1;
|
|
372
|
-
return `cursor-replay-${Date.now()}-${cursorNativeReplayCounter}`;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | undefined {
|
|
376
|
-
return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function getPendingCursorLiveRun(context: Context): CursorLiveRun | undefined {
|
|
380
|
-
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
|
381
|
-
const message = context.messages[index];
|
|
382
|
-
if (message.role !== "toolResult") break;
|
|
383
|
-
const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
|
|
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
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return undefined;
|
|
393
|
-
}
|
|
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
|
-
|
|
407
|
-
function splitTextIntoReplayDeltas(text: string): string[] {
|
|
408
|
-
const deltas: string[] = [];
|
|
409
|
-
let remaining = text;
|
|
410
|
-
while (remaining.length > 0) {
|
|
411
|
-
if (remaining.length <= 96) {
|
|
412
|
-
deltas.push(remaining);
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
const boundary = Math.max(48, remaining.lastIndexOf(" ", 96));
|
|
416
|
-
deltas.push(remaining.slice(0, boundary));
|
|
417
|
-
remaining = remaining.slice(boundary);
|
|
418
|
-
}
|
|
419
|
-
return deltas;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function emitTextDeltas(
|
|
423
|
-
stream: AssistantMessageEventStream,
|
|
424
|
-
partial: AssistantMessage,
|
|
425
|
-
deltas: string[],
|
|
426
|
-
): Promise<string> {
|
|
427
|
-
if (deltas.length === 0) return "";
|
|
428
|
-
const contentIndex = partial.content.length;
|
|
429
|
-
partial.content.push({ type: "text", text: "" });
|
|
430
|
-
stream.push({ type: "text_start", contentIndex, partial });
|
|
431
|
-
const block = partial.content[contentIndex];
|
|
432
|
-
if (block.type !== "text") return "";
|
|
433
|
-
for (const delta of deltas) {
|
|
434
|
-
block.text += delta;
|
|
435
|
-
stream.push({ type: "text_delta", contentIndex, delta, partial });
|
|
436
|
-
await Promise.resolve();
|
|
437
|
-
}
|
|
438
|
-
stream.push({ type: "text_end", contentIndex, content: block.text, partial });
|
|
439
|
-
return block.text;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function notifyCursorNativeRun(run: CursorLiveRun): void {
|
|
443
|
-
for (const waiter of run.waiters) waiter();
|
|
444
|
-
run.waiters.clear();
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function queueCursorNativeEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void {
|
|
448
|
-
run.pendingEvents.push(event);
|
|
449
|
-
notifyCursorNativeRun(run);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function clearCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
453
|
-
if (!run.idleDisposeTimer) return;
|
|
454
|
-
clearTimeout(run.idleDisposeTimer);
|
|
455
|
-
run.idleDisposeTimer = undefined;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function scheduleCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
459
|
-
if (run.disposed) return;
|
|
460
|
-
clearCursorNativeRunIdleDispose(run);
|
|
461
|
-
run.idleDisposeTimer = setTimeout(() => {
|
|
462
|
-
void releaseCursorLiveRun(run);
|
|
463
|
-
}, cursorNativeReplayIdleDisposeMs);
|
|
464
|
-
run.idleDisposeTimer.unref?.();
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function isCursorNativeRunReady(run: CursorLiveRun): boolean {
|
|
468
|
-
return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async function waitForCursorNativeRunProgress(run: CursorLiveRun, signal?: AbortSignal): Promise<void> {
|
|
472
|
-
if (signal?.aborted) throw new CursorAbortError();
|
|
473
|
-
if (isCursorNativeRunReady(run)) return;
|
|
474
|
-
await new Promise<void>((resolve, reject) => {
|
|
475
|
-
let waiter: (() => void) | undefined;
|
|
476
|
-
const cleanup = (): void => {
|
|
477
|
-
if (waiter) run.waiters.delete(waiter);
|
|
478
|
-
signal?.removeEventListener("abort", onAbort);
|
|
479
|
-
};
|
|
480
|
-
const onAbort = (): void => {
|
|
481
|
-
cleanup();
|
|
482
|
-
reject(new CursorAbortError());
|
|
483
|
-
};
|
|
484
|
-
waiter = (): void => {
|
|
485
|
-
cleanup();
|
|
486
|
-
resolve();
|
|
487
|
-
};
|
|
488
|
-
run.waiters.add(waiter);
|
|
489
|
-
if (signal?.aborted) {
|
|
490
|
-
onAbort();
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<void> {
|
|
498
|
-
const eventType = run.pendingEvents[0]?.type;
|
|
499
|
-
if (eventType !== "tool" && eventType !== "bridge-tool") return;
|
|
500
|
-
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function closeCursorNativeThinkingBlock(turn: CursorLiveTurnState): void {
|
|
504
|
-
if (turn.thinkingContentIndex < 0) return;
|
|
505
|
-
const block = turn.partial.content[turn.thinkingContentIndex];
|
|
506
|
-
if (block.type === "thinking") {
|
|
507
|
-
turn.stream.push({
|
|
508
|
-
type: "thinking_end",
|
|
509
|
-
contentIndex: turn.thinkingContentIndex,
|
|
510
|
-
content: block.thinking,
|
|
511
|
-
partial: turn.partial,
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
turn.thinkingContentIndex = -1;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function closeCursorNativeTextBlock(turn: CursorLiveTurnState): string {
|
|
518
|
-
if (turn.textContentIndex < 0) return "";
|
|
519
|
-
const contentIndex = turn.textContentIndex;
|
|
520
|
-
const block = turn.partial.content[contentIndex];
|
|
521
|
-
turn.textContentIndex = -1;
|
|
522
|
-
if (block.type !== "text") return "";
|
|
523
|
-
turn.stream.push({
|
|
524
|
-
type: "text_end",
|
|
525
|
-
contentIndex,
|
|
526
|
-
content: block.text,
|
|
527
|
-
partial: turn.partial,
|
|
528
|
-
});
|
|
529
|
-
return block.text;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function closeCursorNativeTurnBlocks(turn: CursorLiveTurnState): string {
|
|
533
|
-
closeCursorNativeThinkingBlock(turn);
|
|
534
|
-
return closeCursorNativeTextBlock(turn);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function emitCursorNativeThinkingDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
538
|
-
closeCursorNativeTextBlock(turn);
|
|
539
|
-
if (turn.thinkingContentIndex < 0) {
|
|
540
|
-
turn.thinkingContentIndex = turn.partial.content.length;
|
|
541
|
-
turn.partial.content.push({ type: "thinking", thinking: "" });
|
|
542
|
-
turn.stream.push({ type: "thinking_start", contentIndex: turn.thinkingContentIndex, partial: turn.partial });
|
|
543
|
-
}
|
|
544
|
-
const block = turn.partial.content[turn.thinkingContentIndex];
|
|
545
|
-
if (block.type !== "thinking") return;
|
|
546
|
-
block.thinking += delta;
|
|
547
|
-
turn.stream.push({ type: "thinking_delta", contentIndex: turn.thinkingContentIndex, delta, partial: turn.partial });
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function emitCursorNativeTextDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
551
|
-
closeCursorNativeThinkingBlock(turn);
|
|
552
|
-
if (turn.textContentIndex < 0) {
|
|
553
|
-
turn.textContentIndex = turn.partial.content.length;
|
|
554
|
-
turn.partial.content.push({ type: "text", text: "" });
|
|
555
|
-
turn.stream.push({ type: "text_start", contentIndex: turn.textContentIndex, partial: turn.partial });
|
|
556
|
-
}
|
|
557
|
-
const block = turn.partial.content[turn.textContentIndex];
|
|
558
|
-
if (block.type !== "text") return;
|
|
559
|
-
block.text += delta;
|
|
560
|
-
turn.stream.push({ type: "text_delta", contentIndex: turn.textContentIndex, delta, partial: turn.partial });
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function emitCursorLiveQueuedEvent(
|
|
564
|
-
turn: CursorLiveTurnState,
|
|
565
|
-
event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
|
|
566
|
-
run?: CursorLiveRun,
|
|
567
|
-
): void {
|
|
568
|
-
if (event.type === "thinking-delta") {
|
|
569
|
-
emitCursorNativeThinkingDelta(turn, event.text);
|
|
570
|
-
} else if (event.type === "thinking-completed") {
|
|
571
|
-
closeCursorNativeThinkingBlock(turn);
|
|
572
|
-
} else if (event.type === "text-delta") {
|
|
573
|
-
turn.emittedText += event.text;
|
|
574
|
-
if (run) run.emittedText += event.text;
|
|
575
|
-
emitCursorNativeTextDelta(turn, event.text);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function collectCursorNativeToolBatch(run: CursorLiveRun): CursorNativeToolDisplayItem[] {
|
|
580
|
-
const tools: CursorNativeToolDisplayItem[] = [];
|
|
581
|
-
while (run.pendingEvents[0]?.type === "tool") {
|
|
582
|
-
const event = run.pendingEvents.shift();
|
|
583
|
-
if (event?.type === "tool") tools.push(event.tool);
|
|
584
|
-
}
|
|
585
|
-
return tools;
|
|
586
|
-
}
|
|
587
|
-
|
|
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 {
|
|
649
|
-
// Native replay can split one Cursor run into multiple pi turns; count prompt input once.
|
|
650
|
-
const taken = takeCursorLiveTurnInputTokens(run.accounting, toolResultInputTokens);
|
|
651
|
-
run.accounting = taken.state;
|
|
652
|
-
return taken.sessionInputTokens;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function emitCursorNativeToolUseTurn(
|
|
656
|
-
stream: AssistantMessageEventStream,
|
|
657
|
-
partial: AssistantMessage,
|
|
658
|
-
model: Model<Api>,
|
|
659
|
-
context: Context,
|
|
660
|
-
run: CursorLiveRun,
|
|
661
|
-
toolResultInputTokens: number,
|
|
662
|
-
tools: CursorNativeToolDisplayItem[],
|
|
663
|
-
): void {
|
|
664
|
-
const shouldTerminate = run.done && !run.finalText?.trim() && run.pendingEvents.length === 0;
|
|
665
|
-
for (const tool of tools) {
|
|
666
|
-
const contentIndex = partial.content.length;
|
|
667
|
-
partial.content.push({
|
|
668
|
-
type: "toolCall",
|
|
669
|
-
id: tool.id,
|
|
670
|
-
name: tool.toolName,
|
|
671
|
-
arguments: tool.args,
|
|
672
|
-
});
|
|
673
|
-
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
674
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(tool.args), partial });
|
|
675
|
-
const block = partial.content[contentIndex];
|
|
676
|
-
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
677
|
-
if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
|
|
678
|
-
run.recordedToolDisplayIds.push(tool.id);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
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));
|
|
710
|
-
partial.stopReason = "toolUse";
|
|
711
|
-
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
712
|
-
scheduleCursorNativeRunIdleDispose(run);
|
|
713
|
-
}
|
|
714
|
-
|
|
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> {
|
|
725
|
-
if (run.disposed) return;
|
|
726
|
-
const abandoned = !isSuccessfulCursorLiveRun(run);
|
|
727
|
-
run.disposed = true;
|
|
728
|
-
pendingCursorLiveRuns.delete(run.id);
|
|
729
|
-
clearCursorNativeRunIdleDispose(run);
|
|
730
|
-
run.bridgeRun?.cancel("Cursor live run released");
|
|
731
|
-
for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
|
|
732
|
-
run.recordedToolDisplayIds = [];
|
|
733
|
-
run.waiters.clear();
|
|
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);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
async function emitCursorNativeRunNextTurn(
|
|
755
|
-
stream: AssistantMessageEventStream,
|
|
756
|
-
partial: AssistantMessage,
|
|
757
|
-
model: Model<Api>,
|
|
758
|
-
context: Context,
|
|
759
|
-
run: CursorLiveRun,
|
|
760
|
-
toolResultInputTokens: number,
|
|
761
|
-
signal?: AbortSignal,
|
|
762
|
-
): Promise<void> {
|
|
763
|
-
const turn: CursorLiveTurnState = {
|
|
764
|
-
stream,
|
|
765
|
-
partial,
|
|
766
|
-
thinkingContentIndex: -1,
|
|
767
|
-
textContentIndex: -1,
|
|
768
|
-
emittedText: "",
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
while (true) {
|
|
772
|
-
while (run.pendingEvents.length > 0) {
|
|
773
|
-
const event = run.pendingEvents[0];
|
|
774
|
-
if (event.type === "tool") {
|
|
775
|
-
await settleCursorLiveToolBatch(run);
|
|
776
|
-
if (signal?.aborted) throw new CursorAbortError();
|
|
777
|
-
closeCursorNativeTurnBlocks(turn);
|
|
778
|
-
const tools = collectCursorNativeToolBatch(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);
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
run.pendingEvents.shift();
|
|
791
|
-
emitCursorLiveQueuedEvent(turn, event, run);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
if (run.cancelled) {
|
|
795
|
-
partial.stopReason = "aborted";
|
|
796
|
-
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
797
|
-
await releaseCursorLiveRun(run);
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
if (run.errorMessage) {
|
|
801
|
-
partial.stopReason = "error";
|
|
802
|
-
partial.errorMessage = run.errorMessage;
|
|
803
|
-
stream.push({ type: "error", reason: "error", error: partial });
|
|
804
|
-
await releaseCursorLiveRun(run);
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
if (run.done) {
|
|
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));
|
|
812
|
-
}
|
|
813
|
-
applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
|
|
814
|
-
partial.stopReason = "stop";
|
|
815
|
-
stream.push({ type: "done", reason: "stop", message: partial });
|
|
816
|
-
await releaseCursorLiveRun(run);
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
await waitForCursorNativeRunProgress(run, signal);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
async function replayPendingCursorLiveRun(
|
|
825
|
-
stream: AssistantMessageEventStream,
|
|
826
|
-
partial: AssistantMessage,
|
|
827
|
-
model: Model<Api>,
|
|
828
|
-
context: Context,
|
|
829
|
-
signal?: AbortSignal,
|
|
830
|
-
): Promise<boolean> {
|
|
831
|
-
const run = getPendingCursorLiveRun(context);
|
|
832
|
-
if (!run) return false;
|
|
833
|
-
clearCursorNativeRunIdleDispose(run);
|
|
834
|
-
const consumed = consumeCursorLiveRunToolResults(run, context);
|
|
835
|
-
run.bridgeRun?.resolveToolResults(consumed.toolResults);
|
|
836
|
-
try {
|
|
837
|
-
await emitCursorNativeRunNextTurn(stream, partial, model, context, run, consumed.toolResultInputTokens, signal);
|
|
838
|
-
} catch (error) {
|
|
839
|
-
if (error instanceof CursorAbortError) await releaseCursorLiveRun(run);
|
|
840
|
-
throw error;
|
|
841
|
-
}
|
|
842
|
-
return true;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
134
|
export function streamCursor(
|
|
846
135
|
model: Model<Api>,
|
|
847
136
|
context: Context,
|
|
@@ -864,13 +153,13 @@ export function streamCursor(
|
|
|
864
153
|
|
|
865
154
|
try {
|
|
866
155
|
const throwIfAborted = (): void => {
|
|
867
|
-
if (options?.signal?.aborted) throw new
|
|
156
|
+
if (options?.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
868
157
|
};
|
|
869
158
|
|
|
870
159
|
stream.push({ type: "start", partial });
|
|
871
160
|
throwIfAborted();
|
|
872
161
|
|
|
873
|
-
if (await
|
|
162
|
+
if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal)) === "stream_ended") {
|
|
874
163
|
stream.end();
|
|
875
164
|
return;
|
|
876
165
|
}
|
|
@@ -895,7 +184,7 @@ export function streamCursor(
|
|
|
895
184
|
settingSources,
|
|
896
185
|
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
897
186
|
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
898
|
-
|
|
187
|
+
cursorLiveRuns.queueEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
899
188
|
} else {
|
|
900
189
|
queuedBridgeRequestsBeforeLiveRun.push(request);
|
|
901
190
|
}
|
|
@@ -921,354 +210,40 @@ export function streamCursor(
|
|
|
921
210
|
}
|
|
922
211
|
const sessionBridgeRun = sessionAgentLease.bridgeRun;
|
|
923
212
|
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
924
|
-
let thinkingContentIndex = -1;
|
|
925
|
-
let activityTraceChars = 0;
|
|
926
|
-
let activityTraceTruncated = false;
|
|
927
|
-
let nativeToolDisplayCounter = 0;
|
|
928
|
-
let textContentIndex = -1;
|
|
929
213
|
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
214
|
+
const activeToolNames = getActiveContextToolNames(context);
|
|
930
215
|
const nativeReplayId = createCursorNativeReplayId();
|
|
931
216
|
const textDeltas: string[] = [];
|
|
932
|
-
let nativeToolReplayStarted = false;
|
|
933
217
|
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
934
218
|
const liveRun: CursorLiveRun | undefined = useLiveRun
|
|
935
|
-
? {
|
|
219
|
+
? cursorLiveRuns.start({
|
|
936
220
|
id: useNativeToolReplay ? nativeReplayId : bridgeRun!.id,
|
|
937
221
|
agent,
|
|
938
222
|
bridgeRun,
|
|
939
223
|
sessionBridgeRun,
|
|
940
224
|
sessionAgentScopeKey,
|
|
941
|
-
|
|
942
|
-
pendingEvents: [],
|
|
225
|
+
promptInputTokens,
|
|
943
226
|
textDeltas,
|
|
944
|
-
|
|
945
|
-
recordedToolDisplayIds: [],
|
|
946
|
-
done: false,
|
|
947
|
-
cancelled: false,
|
|
948
|
-
disposed: false,
|
|
949
|
-
waiters: new Set(),
|
|
950
|
-
}
|
|
227
|
+
})
|
|
951
228
|
: undefined;
|
|
952
229
|
if (liveRun) {
|
|
953
|
-
pendingCursorLiveRuns.set(liveRun.id, liveRun);
|
|
954
230
|
activeLiveRun = liveRun;
|
|
955
231
|
liveRunForBridgeQueue = liveRun;
|
|
956
232
|
for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
|
|
957
|
-
|
|
233
|
+
cursorLiveRuns.queueEvent(liveRun, { type: "bridge-tool", request });
|
|
958
234
|
}
|
|
959
235
|
}
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (textContentIndex < 0) {
|
|
972
|
-
textContentIndex = partial.content.length;
|
|
973
|
-
partial.content.push({ type: "text", text: "" });
|
|
974
|
-
stream.push({ type: "text_start", contentIndex: textContentIndex, partial });
|
|
975
|
-
}
|
|
976
|
-
const block = partial.content[textContentIndex];
|
|
977
|
-
if (block.type !== "text") return;
|
|
978
|
-
block.text += text;
|
|
979
|
-
stream.push({
|
|
980
|
-
type: "text_delta",
|
|
981
|
-
contentIndex: textContentIndex,
|
|
982
|
-
delta: text,
|
|
983
|
-
partial,
|
|
984
|
-
});
|
|
985
|
-
};
|
|
986
|
-
|
|
987
|
-
const appendTraceDelta = (text: string): void => {
|
|
988
|
-
if (activityTraceTruncated) return;
|
|
989
|
-
|
|
990
|
-
let delta = text;
|
|
991
|
-
if (activityTraceChars + delta.length > CURSOR_ACTIVITY_TRACE_MAX_CHARS) {
|
|
992
|
-
const remainingChars = Math.max(CURSOR_ACTIVITY_TRACE_MAX_CHARS - activityTraceChars, 0);
|
|
993
|
-
delta = `${delta.slice(0, remainingChars)}\n[Cursor activity trace truncated]\n`;
|
|
994
|
-
activityTraceTruncated = true;
|
|
995
|
-
}
|
|
996
|
-
if (!delta) return;
|
|
997
|
-
|
|
998
|
-
if (thinkingContentIndex < 0) {
|
|
999
|
-
thinkingContentIndex = partial.content.length;
|
|
1000
|
-
partial.content.push({ type: "thinking", thinking: "" });
|
|
1001
|
-
stream.push({ type: "thinking_start", contentIndex: thinkingContentIndex, partial });
|
|
1002
|
-
}
|
|
1003
|
-
const block = partial.content[thinkingContentIndex];
|
|
1004
|
-
if (block.type === "thinking") {
|
|
1005
|
-
block.thinking += delta;
|
|
1006
|
-
activityTraceChars += delta.length;
|
|
1007
|
-
stream.push({
|
|
1008
|
-
type: "thinking_delta",
|
|
1009
|
-
contentIndex: thinkingContentIndex,
|
|
1010
|
-
delta,
|
|
1011
|
-
partial,
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
const appendTraceLine = (text: string): void => {
|
|
1017
|
-
appendTraceDelta(`${text}\n`);
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1020
|
-
const appendTraceBlock = (text: string): void => {
|
|
1021
|
-
closeTraceBlock();
|
|
1022
|
-
appendTraceDelta(text.endsWith("\n") ? text : `${text}\n`);
|
|
1023
|
-
closeTraceBlock();
|
|
1024
|
-
};
|
|
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
|
-
|
|
1036
|
-
const closeTraceBlock = (): void => {
|
|
1037
|
-
if (thinkingContentIndex < 0) return;
|
|
1038
|
-
const block = partial.content[thinkingContentIndex];
|
|
1039
|
-
if (block.type === "thinking") {
|
|
1040
|
-
stream.push({
|
|
1041
|
-
type: "thinking_end",
|
|
1042
|
-
contentIndex: thinkingContentIndex,
|
|
1043
|
-
content: block.thinking,
|
|
1044
|
-
partial,
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
thinkingContentIndex = -1;
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
const flushText = (deltas: string[]): string => {
|
|
1051
|
-
for (const delta of deltas) appendLiveTextDelta(delta);
|
|
1052
|
-
if (textContentIndex < 0) return "";
|
|
1053
|
-
const block = partial.content[textContentIndex];
|
|
1054
|
-
if (block.type !== "text") return "";
|
|
1055
|
-
stream.push({
|
|
1056
|
-
type: "text_end",
|
|
1057
|
-
contentIndex: textContentIndex,
|
|
1058
|
-
content: block.text,
|
|
1059
|
-
partial,
|
|
1060
|
-
});
|
|
1061
|
-
return block.text;
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
const getToolFingerprint = (value: unknown): string => {
|
|
1065
|
-
try {
|
|
1066
|
-
return JSON.stringify(value);
|
|
1067
|
-
} catch {
|
|
1068
|
-
return String(value);
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
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
|
-
|
|
1135
|
-
const handleCompletedToolCall = (
|
|
1136
|
-
toolCall: unknown,
|
|
1137
|
-
options: { identity?: string; source?: "started" | "fallback" } = {},
|
|
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
|
-
}
|
|
1146
|
-
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
|
|
1147
|
-
const display = buildCursorPiToolDisplay(toolCall, { cwd });
|
|
1148
|
-
const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
|
|
1149
|
-
if (options.identity && completedToolIdentities.has(options.identity)) return;
|
|
1150
|
-
if (options.source === "started") {
|
|
1151
|
-
if (completedFallbackToolFingerprints.has(fingerprint)) return;
|
|
1152
|
-
} else if (completedStartedToolFingerprints.has(fingerprint) || completedFallbackToolFingerprints.has(fingerprint)) {
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
if (options.identity) completedToolIdentities.add(options.identity);
|
|
1156
|
-
if (options.source === "started") {
|
|
1157
|
-
completedStartedToolFingerprints.add(fingerprint);
|
|
1158
|
-
} else {
|
|
1159
|
-
completedFallbackToolFingerprints.add(fingerprint);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const nativeRenderable = canRenderCursorToolNatively(display.toolName);
|
|
1163
|
-
const route = useNativeToolReplay && nativeRenderable && liveRun ? "native_replay" : "trace";
|
|
1164
|
-
|
|
1165
|
-
if (route === "native_replay" && liveRun) {
|
|
1166
|
-
nativeToolReplayStarted = true;
|
|
1167
|
-
const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
|
|
1168
|
-
queueCursorNativeEvent(liveRun, {
|
|
1169
|
-
type: "tool",
|
|
1170
|
-
tool: {
|
|
1171
|
-
...display,
|
|
1172
|
-
id,
|
|
1173
|
-
args: scrubDisplayValue(display.args, resolvedApiKey) as Record<string, unknown>,
|
|
1174
|
-
result: scrubDisplayValue(display.result, resolvedApiKey) as typeof display.result,
|
|
1175
|
-
},
|
|
1176
|
-
});
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
emitCursorToolTrace(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
const onDelta = (args: { update: InteractionUpdate }): void => {
|
|
1184
|
-
const update = args.update;
|
|
1185
|
-
|
|
1186
|
-
if (update.type === "text-delta") {
|
|
1187
|
-
textDeltas.push(update.text);
|
|
1188
|
-
if (liveRun) {
|
|
1189
|
-
queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
|
|
1190
|
-
} else {
|
|
1191
|
-
appendLiveTextDelta(update.text);
|
|
1192
|
-
}
|
|
1193
|
-
} else if (update.type === "thinking-delta") {
|
|
1194
|
-
if (liveRun) {
|
|
1195
|
-
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
|
|
1196
|
-
} else {
|
|
1197
|
-
appendTraceDelta(update.text);
|
|
1198
|
-
}
|
|
1199
|
-
} else if (update.type === "thinking-completed") {
|
|
1200
|
-
if (liveRun) {
|
|
1201
|
-
queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
|
|
1202
|
-
} else {
|
|
1203
|
-
closeTraceBlock();
|
|
1204
|
-
}
|
|
1205
|
-
} else if (update.type === "tool-call-started") {
|
|
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
|
-
}
|
|
1212
|
-
} else if (update.type === "tool-call-completed") {
|
|
1213
|
-
const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
|
|
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, {
|
|
1223
|
-
identity,
|
|
1224
|
-
source: identity ? "started" : "fallback",
|
|
1225
|
-
});
|
|
1226
|
-
} else if (update.type === "shell-output-delta") {
|
|
1227
|
-
const delta = getCursorShellOutputDelta(update);
|
|
1228
|
-
if (delta) appendShellOutputDelta(delta);
|
|
1229
|
-
} else if (update.type === "summary") {
|
|
1230
|
-
const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
|
|
1231
|
-
if (liveRun) {
|
|
1232
|
-
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
|
|
1233
|
-
} else {
|
|
1234
|
-
appendTraceDelta(summary);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
// Cursor turn-ended usage is intentionally not copied into pi usage: the SDK reports
|
|
1238
|
-
// cumulative internal agent/tool/cache tokens, not the replayable pi prompt context.
|
|
1239
|
-
// partial-tool-call, summary-started, summary-completed, turn-ended,
|
|
1240
|
-
// token-delta, step-* are intentionally not surfaced.
|
|
1241
|
-
};
|
|
1242
|
-
|
|
1243
|
-
const onStep = (args: { step: unknown }): void => {
|
|
1244
|
-
const stepType = getObjectField(args.step, "type");
|
|
1245
|
-
const step = getObjectField(args.step, "message") ? args.step : undefined;
|
|
1246
|
-
const rawStepToolCall = getObjectField(step, "message");
|
|
1247
|
-
if (stepType !== "toolCall") return;
|
|
1248
|
-
const toolCall = rawStepToolCall;
|
|
1249
|
-
const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
|
|
1250
|
-
if (toolCall) {
|
|
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,
|
|
1268
|
-
});
|
|
1269
|
-
if (matchedStartedCallId && matchedStartedCallId !== stepId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
|
|
1270
|
-
}
|
|
1271
|
-
};
|
|
236
|
+
const turnCoordinator = new CursorSdkTurnCoordinator({
|
|
237
|
+
stream,
|
|
238
|
+
partial,
|
|
239
|
+
cwd,
|
|
240
|
+
resolvedApiKey,
|
|
241
|
+
liveRun,
|
|
242
|
+
useNativeToolReplay,
|
|
243
|
+
activeToolNames,
|
|
244
|
+
nativeReplayId,
|
|
245
|
+
textDeltas,
|
|
246
|
+
});
|
|
1272
247
|
|
|
1273
248
|
// Handle abort signal
|
|
1274
249
|
let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
|
|
@@ -1284,12 +259,15 @@ export function streamCursor(
|
|
|
1284
259
|
throwIfAborted();
|
|
1285
260
|
run = await agent.send(
|
|
1286
261
|
{ text: prompt.text, images: prompt.images.length > 0 ? prompt.images : undefined },
|
|
1287
|
-
{
|
|
262
|
+
{
|
|
263
|
+
onDelta: (args) => turnCoordinator.handleDelta(args.update),
|
|
264
|
+
onStep: (args) => turnCoordinator.handleStep(args.step),
|
|
265
|
+
},
|
|
1288
266
|
);
|
|
1289
|
-
if (liveRun) liveRun
|
|
267
|
+
if (liveRun) cursorLiveRuns.attachSdkRun(liveRun, run);
|
|
1290
268
|
if (options?.signal?.aborted) {
|
|
1291
269
|
await run.cancel().catch(() => {});
|
|
1292
|
-
throw new
|
|
270
|
+
throw new CursorLiveRunAbortError();
|
|
1293
271
|
}
|
|
1294
272
|
|
|
1295
273
|
if (liveRun) {
|
|
@@ -1297,39 +275,35 @@ export function streamCursor(
|
|
|
1297
275
|
.wait()
|
|
1298
276
|
.then(async (result) => {
|
|
1299
277
|
if (liveRun.disposed) return;
|
|
1300
|
-
discardIncompleteStartedToolCalls();
|
|
278
|
+
turnCoordinator.discardIncompleteStartedToolCalls();
|
|
1301
279
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
1302
280
|
if (liveRun.disposed) return;
|
|
1303
281
|
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
1304
282
|
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
283
|
+
cursorLiveRuns.markFinished(
|
|
284
|
+
liveRun,
|
|
285
|
+
selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, turnCoordinator.planTextCandidate),
|
|
286
|
+
);
|
|
287
|
+
} else if (result.status === "cancelled" || options?.signal?.aborted) {
|
|
288
|
+
cursorLiveRuns.markCancelled(liveRun);
|
|
1305
289
|
} else {
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1308
|
-
liveRun.cancelled = result.status === "cancelled";
|
|
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);
|
|
290
|
+
cursorLiveRuns.markError(liveRun, sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey));
|
|
1313
291
|
}
|
|
1314
|
-
liveRun.done = true;
|
|
1315
|
-
notifyCursorNativeRun(liveRun);
|
|
1316
|
-
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
1317
292
|
})
|
|
1318
293
|
.catch(async (error: unknown) => {
|
|
1319
294
|
if (liveRun.disposed) return;
|
|
1320
|
-
|
|
1321
|
-
liveRun.errorMessage = sanitizeError(error, resolvedApiKey ?? options?.apiKey);
|
|
1322
|
-
notifyCursorNativeRun(liveRun);
|
|
1323
|
-
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
295
|
+
cursorLiveRuns.markError(liveRun, sanitizeError(error, resolvedApiKey ?? options?.apiKey));
|
|
1324
296
|
});
|
|
1325
297
|
|
|
1326
298
|
try {
|
|
1327
|
-
await
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
299
|
+
await cursorLiveRuns.withRunLease(liveRun, options?.signal, async () => {
|
|
300
|
+
await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
|
|
301
|
+
await settleCursorLiveToolBatch(liveRun);
|
|
302
|
+
turnCoordinator.closeTraceBlock();
|
|
303
|
+
await drainCursorLiveRunTurn(stream, partial, model, context, liveRun, 0, { mode: "emit", signal: options?.signal });
|
|
304
|
+
});
|
|
1331
305
|
} catch (error) {
|
|
1332
|
-
if (error instanceof
|
|
306
|
+
if (error instanceof CursorLiveRunAbortError) await cursorLiveRuns.release(liveRun);
|
|
1333
307
|
throw error;
|
|
1334
308
|
}
|
|
1335
309
|
agent = null;
|
|
@@ -1337,12 +311,12 @@ export function streamCursor(
|
|
|
1337
311
|
}
|
|
1338
312
|
|
|
1339
313
|
const result = await run.wait();
|
|
1340
|
-
discardIncompleteStartedToolCalls();
|
|
314
|
+
turnCoordinator.discardIncompleteStartedToolCalls();
|
|
1341
315
|
await cacheSdkContextWindow(agent.agentId, model.id);
|
|
1342
316
|
|
|
1343
317
|
// Close any open thinking/activity trace, then use the final run result only when
|
|
1344
318
|
// Cursor did not stream text deltas.
|
|
1345
|
-
closeTraceBlock();
|
|
319
|
+
turnCoordinator.closeTraceBlock();
|
|
1346
320
|
|
|
1347
321
|
if (result.status === "cancelled") {
|
|
1348
322
|
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
@@ -1355,17 +329,17 @@ export function streamCursor(
|
|
|
1355
329
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
1356
330
|
} else {
|
|
1357
331
|
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
1358
|
-
const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""),
|
|
332
|
+
const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""), turnCoordinator.planTextCandidate, {
|
|
1359
333
|
allowPartialPrefix: true,
|
|
1360
334
|
});
|
|
1361
|
-
flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
|
|
335
|
+
turnCoordinator.flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
|
|
1362
336
|
applyCursorApproximateUsage(partial, model, context, promptInputTokens);
|
|
1363
337
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
1364
338
|
}
|
|
1365
339
|
} catch (error) {
|
|
1366
|
-
if (activeLiveRun && !activeLiveRun.disposed) await
|
|
340
|
+
if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun);
|
|
1367
341
|
else await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1368
|
-
if (error instanceof
|
|
342
|
+
if (error instanceof CursorLiveRunAbortError) {
|
|
1369
343
|
partial.stopReason = "aborted";
|
|
1370
344
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
1371
345
|
} else {
|
|
@@ -1389,12 +363,12 @@ export function streamCursor(
|
|
|
1389
363
|
|
|
1390
364
|
export const __testUtils = {
|
|
1391
365
|
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
1392
|
-
pendingCursorNativeRunCount:
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
366
|
+
pendingCursorNativeRunCount: cursorLiveRuns.count,
|
|
367
|
+
getPendingCursorLiveRun,
|
|
368
|
+
getActiveCursorLiveRunForScope: cursorLiveRuns.getActiveForScope,
|
|
369
|
+
hasTrailingUserMessagesAfterToolResults,
|
|
370
|
+
setCursorNativeReplayIdleDisposeMs,
|
|
371
|
+
resetCursorNativeReplayIdleDisposeMs,
|
|
372
|
+
releaseAllPendingCursorLiveRunsForTests,
|
|
1399
373
|
resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
|
|
1400
374
|
};
|