pi-cursor-sdk 0.1.15 → 0.1.17
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 +56 -1
- package/README.md +20 -8
- package/docs/cursor-live-smoke-checklist.md +267 -0
- package/docs/cursor-model-ux-spec.md +15 -5
- package/docs/cursor-native-tool-replay.md +16 -5
- package/package.json +12 -5
- package/scripts/steering-rpc-smoke.mjs +238 -0
- package/scripts/tmux-live-smoke.sh +418 -0
- package/scripts/validate-smoke-jsonl.mjs +152 -0
- package/src/context.ts +180 -5
- package/src/cursor-bridge-contract.ts +27 -0
- package/src/cursor-edit-diff.ts +11 -0
- package/src/cursor-env-boolean.ts +22 -0
- package/src/cursor-live-run-accounting.ts +65 -0
- package/src/cursor-live-run-coordinator.ts +483 -0
- package/src/cursor-native-tool-display-registration.ts +93 -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 -639
- 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 +77 -602
- package/src/cursor-provider-live-run-drain.ts +379 -0
- package/src/cursor-provider-turn-coordinator.ts +456 -0
- package/src/cursor-provider.ts +133 -1092
- package/src/cursor-question-tool.ts +7 -2
- 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-session-agent.ts +372 -0
- package/src/cursor-session-cwd.ts +14 -19
- package/src/cursor-session-scope.ts +65 -0
- package/src/cursor-state.ts +38 -10
- 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-usage-accounting.ts +71 -0
- package/src/index.ts +20 -3
package/src/cursor-provider.ts
CHANGED
|
@@ -7,28 +7,47 @@ import {
|
|
|
7
7
|
type SimpleStreamOptions,
|
|
8
8
|
type AssistantMessage,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
10
|
import { Agent, createAgentPlatform } from "@cursor/sdk";
|
|
12
|
-
import type {
|
|
11
|
+
import type { SDKAgent, SettingSource } from "@cursor/sdk";
|
|
13
12
|
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
14
|
-
import {
|
|
13
|
+
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
|
|
14
|
+
import { buildCursorSendPrompt } from "./context.js";
|
|
15
|
+
import {
|
|
16
|
+
acquireSessionCursorAgent,
|
|
17
|
+
commitSessionAgentSend,
|
|
18
|
+
disposeAllSessionCursorAgents,
|
|
19
|
+
resetSessionCursorAgent,
|
|
20
|
+
} from "./cursor-session-agent.js";
|
|
15
21
|
import {
|
|
16
|
-
getRegisteredCursorPiToolBridge,
|
|
17
22
|
type CursorPiBridgeToolRequest,
|
|
18
23
|
type CursorPiToolBridgeRun,
|
|
19
24
|
} from "./cursor-pi-tool-bridge.js";
|
|
25
|
+
import {
|
|
26
|
+
applyCursorApproximateUsage,
|
|
27
|
+
estimateCursorPromptInputTokens,
|
|
28
|
+
getCursorPromptOptions,
|
|
29
|
+
} from "./cursor-usage-accounting.js";
|
|
20
30
|
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
31
|
+
import { CursorLiveRunAbortError, type CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
32
|
+
import {
|
|
33
|
+
abandonSessionCursorAgent,
|
|
34
|
+
createCursorNativeReplayId,
|
|
35
|
+
cursorLiveRuns,
|
|
36
|
+
drainCursorLiveRunTurn,
|
|
37
|
+
drainExistingCursorLiveRunBeforeSend,
|
|
38
|
+
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
39
|
+
getPendingCursorLiveRun,
|
|
40
|
+
hasTrailingUserMessagesAfterToolResults,
|
|
41
|
+
resetCursorNativeReplayIdleDisposeMs,
|
|
42
|
+
selectCursorFinalText,
|
|
43
|
+
setCursorNativeReplayIdleDisposeMs,
|
|
44
|
+
settleCursorLiveToolBatch,
|
|
45
|
+
} from "./cursor-provider-live-run-drain.js";
|
|
21
46
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
22
47
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
23
48
|
import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
canRenderCursorToolNatively,
|
|
27
|
-
isCursorNativeToolDisplayRuntimeEnabled,
|
|
28
|
-
deleteCursorNativeToolDisplay,
|
|
29
|
-
recordCursorNativeToolDisplay,
|
|
30
|
-
type CursorNativeToolDisplayItem,
|
|
31
|
-
} from "./cursor-native-tool-display.js";
|
|
49
|
+
import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
50
|
+
import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display.js";
|
|
32
51
|
|
|
33
52
|
function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
34
53
|
return {
|
|
@@ -50,13 +69,6 @@ function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
|
50
69
|
};
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
class CursorAbortError extends Error {
|
|
54
|
-
constructor() {
|
|
55
|
-
super("aborted");
|
|
56
|
-
this.name = "CursorAbortError";
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
72
|
const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
61
73
|
const MISSING_API_KEY_MESSAGE =
|
|
62
74
|
"Cursor SDK runs require a Cursor API key. Run /login -> Use an API key -> Cursor, set CURSOR_API_KEY before starting pi, or restart pi with --api-key.";
|
|
@@ -64,69 +76,10 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
64
76
|
"Cursor SDK request failed. The API key may be missing, invalid, or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
65
77
|
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
66
78
|
"Cursor SDK request failed because the API key may be invalid or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
67
|
-
const APPROX_CHARS_PER_TOKEN = 4;
|
|
68
|
-
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
69
|
-
const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
|
|
70
|
-
const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
71
|
-
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
72
79
|
const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
73
|
-
const cursorSdkOutputSuppression = new AsyncLocalStorage<boolean>();
|
|
74
|
-
|
|
75
|
-
type CursorLiveQueuedEvent =
|
|
76
|
-
| { type: "thinking-delta"; text: string }
|
|
77
|
-
| { type: "thinking-completed" }
|
|
78
|
-
| { type: "text-delta"; text: string }
|
|
79
|
-
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
80
|
-
| { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
agent: SDKAgent;
|
|
85
|
-
bridgeRun?: CursorPiToolBridgeRun;
|
|
86
|
-
promptInputTokens: number;
|
|
87
|
-
promptInputTokensReported: boolean;
|
|
88
|
-
pendingEvents: CursorLiveQueuedEvent[];
|
|
89
|
-
textDeltas: string[];
|
|
90
|
-
emittedText: string;
|
|
91
|
-
recordedToolDisplayIds: string[];
|
|
92
|
-
finalText?: string;
|
|
93
|
-
done: boolean;
|
|
94
|
-
cancelled: boolean;
|
|
95
|
-
disposed: boolean;
|
|
96
|
-
errorMessage?: string;
|
|
97
|
-
idleDisposeTimer?: ReturnType<typeof setTimeout>;
|
|
98
|
-
waiters: Set<() => void>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface CursorLiveTurnState {
|
|
102
|
-
stream: AssistantMessageEventStream;
|
|
103
|
-
partial: AssistantMessage;
|
|
104
|
-
thinkingContentIndex: number;
|
|
105
|
-
textContentIndex: number;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let cursorNativeReplayCounter = 0;
|
|
109
|
-
let cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
110
|
-
const pendingCursorLiveRuns = new Map<string, CursorLiveRun>();
|
|
111
|
-
|
|
112
|
-
function escapeRegExp(value: string): string {
|
|
113
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function scrubSensitiveText(text: string, apiKey?: string): string {
|
|
117
|
-
let scrubbed = text;
|
|
118
|
-
const trimmedKey = apiKey?.trim();
|
|
119
|
-
if (trimmedKey) {
|
|
120
|
-
scrubbed = scrubbed.replace(new RegExp(escapeRegExp(trimmedKey), "g"), "[redacted]");
|
|
121
|
-
}
|
|
122
|
-
return scrubbed
|
|
123
|
-
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
124
|
-
.replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
|
|
125
|
-
.replace(
|
|
126
|
-
/((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
|
|
127
|
-
"$1[redacted]",
|
|
128
|
-
);
|
|
129
|
-
}
|
|
81
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
82
|
+
import { hasUsableText } from "./cursor-record-utils.js";
|
|
130
83
|
|
|
131
84
|
function isGenericErrorMessage(message: string): boolean {
|
|
132
85
|
const normalized = message.trim().toLowerCase();
|
|
@@ -165,92 +118,6 @@ function sanitizeError(error: unknown, apiKey?: string): string {
|
|
|
165
118
|
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
166
119
|
}
|
|
167
120
|
|
|
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
|
-
|
|
240
|
-
function getObjectField(value: unknown, field: string): unknown {
|
|
241
|
-
if (!value || typeof value !== "object") return undefined;
|
|
242
|
-
return (value as Record<string, unknown>)[field];
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function getCursorToolName(toolCall: unknown): string {
|
|
246
|
-
if (!toolCall || typeof toolCall !== "object") return "unknown";
|
|
247
|
-
const data = toolCall as Record<string, unknown>;
|
|
248
|
-
if (typeof data.name === "string") return data.name;
|
|
249
|
-
if (typeof data.type === "string") return data.type;
|
|
250
|
-
if (typeof data.toolName === "string") return data.toolName;
|
|
251
|
-
return "unknown";
|
|
252
|
-
}
|
|
253
|
-
|
|
254
121
|
async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<void> {
|
|
255
122
|
try {
|
|
256
123
|
const platform = await createAgentPlatform();
|
|
@@ -262,518 +129,6 @@ async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<
|
|
|
262
129
|
}
|
|
263
130
|
}
|
|
264
131
|
|
|
265
|
-
function estimateTextTokens(text: string): number {
|
|
266
|
-
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function estimatePromptInputTokens(prompt: CursorPrompt): number {
|
|
270
|
-
return estimateTextTokens(prompt.text) + prompt.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function getPromptInputTokenBudget(model: Model<Api>): number {
|
|
274
|
-
const outputReserveTokens = Math.min(model.maxTokens, Math.max(1, Math.floor(model.contextWindow * 0.2)));
|
|
275
|
-
return Math.max(1, model.contextWindow - outputReserveTokens);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function setApproximateUsage(partial: AssistantMessage, promptInputTokens: number, outputText: string): void {
|
|
279
|
-
partial.usage.input = promptInputTokens;
|
|
280
|
-
partial.usage.output = estimateTextTokens(outputText);
|
|
281
|
-
partial.usage.cacheRead = 0;
|
|
282
|
-
partial.usage.cacheWrite = 0;
|
|
283
|
-
partial.usage.totalTokens = partial.usage.input + partial.usage.output;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function sanitizeSingleLine(value: string): string {
|
|
287
|
-
return value.replace(/\s+/g, " ").trim();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function truncateSingleLine(value: string, maxLength = 240): string {
|
|
291
|
-
const sanitized = sanitizeSingleLine(value);
|
|
292
|
-
return sanitized.length > maxLength ? `${sanitized.slice(0, maxLength - 1)}…` : sanitized;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function formatCursorToolName(toolCall: unknown): string {
|
|
296
|
-
return truncateSingleLine(getCursorToolName(toolCall), 80) || "unknown";
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function hasUsableText(value: string | undefined): value is string {
|
|
300
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
301
|
-
}
|
|
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
|
-
|
|
360
|
-
function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
|
|
361
|
-
if (typeof value === "string") return scrubSensitiveText(value, apiKey);
|
|
362
|
-
if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
|
|
363
|
-
if (value && typeof value === "object") {
|
|
364
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, scrubDisplayValue(entry, apiKey)]));
|
|
365
|
-
}
|
|
366
|
-
return value;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function createCursorNativeReplayId(): string {
|
|
370
|
-
cursorNativeReplayCounter += 1;
|
|
371
|
-
return `cursor-replay-${Date.now()}-${cursorNativeReplayCounter}`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | undefined {
|
|
375
|
-
return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function getPendingCursorLiveRun(context: Context): CursorLiveRun | undefined {
|
|
379
|
-
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
|
380
|
-
const message = context.messages[index];
|
|
381
|
-
if (message.role !== "toolResult") break;
|
|
382
|
-
const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
|
|
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
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return undefined;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function splitTextIntoReplayDeltas(text: string): string[] {
|
|
395
|
-
const deltas: string[] = [];
|
|
396
|
-
let remaining = text;
|
|
397
|
-
while (remaining.length > 0) {
|
|
398
|
-
if (remaining.length <= 96) {
|
|
399
|
-
deltas.push(remaining);
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
const boundary = Math.max(48, remaining.lastIndexOf(" ", 96));
|
|
403
|
-
deltas.push(remaining.slice(0, boundary));
|
|
404
|
-
remaining = remaining.slice(boundary);
|
|
405
|
-
}
|
|
406
|
-
return deltas;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function emitTextDeltas(
|
|
410
|
-
stream: AssistantMessageEventStream,
|
|
411
|
-
partial: AssistantMessage,
|
|
412
|
-
deltas: string[],
|
|
413
|
-
): Promise<string> {
|
|
414
|
-
if (deltas.length === 0) return "";
|
|
415
|
-
const contentIndex = partial.content.length;
|
|
416
|
-
partial.content.push({ type: "text", text: "" });
|
|
417
|
-
stream.push({ type: "text_start", contentIndex, partial });
|
|
418
|
-
const block = partial.content[contentIndex];
|
|
419
|
-
if (block.type !== "text") return "";
|
|
420
|
-
for (const delta of deltas) {
|
|
421
|
-
block.text += delta;
|
|
422
|
-
stream.push({ type: "text_delta", contentIndex, delta, partial });
|
|
423
|
-
await Promise.resolve();
|
|
424
|
-
}
|
|
425
|
-
stream.push({ type: "text_end", contentIndex, content: block.text, partial });
|
|
426
|
-
return block.text;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function notifyCursorNativeRun(run: CursorLiveRun): void {
|
|
430
|
-
for (const waiter of run.waiters) waiter();
|
|
431
|
-
run.waiters.clear();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function queueCursorNativeEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void {
|
|
435
|
-
run.pendingEvents.push(event);
|
|
436
|
-
notifyCursorNativeRun(run);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function clearCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
440
|
-
if (!run.idleDisposeTimer) return;
|
|
441
|
-
clearTimeout(run.idleDisposeTimer);
|
|
442
|
-
run.idleDisposeTimer = undefined;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function scheduleCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
446
|
-
if (run.disposed) return;
|
|
447
|
-
clearCursorNativeRunIdleDispose(run);
|
|
448
|
-
run.idleDisposeTimer = setTimeout(() => {
|
|
449
|
-
void disposeCursorNativeRun(run);
|
|
450
|
-
}, cursorNativeReplayIdleDisposeMs);
|
|
451
|
-
run.idleDisposeTimer.unref?.();
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function isCursorNativeRunReady(run: CursorLiveRun): boolean {
|
|
455
|
-
return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async function waitForCursorNativeRunProgress(run: CursorLiveRun, signal?: AbortSignal): Promise<void> {
|
|
459
|
-
if (signal?.aborted) throw new CursorAbortError();
|
|
460
|
-
if (isCursorNativeRunReady(run)) return;
|
|
461
|
-
await new Promise<void>((resolve, reject) => {
|
|
462
|
-
let waiter: (() => void) | undefined;
|
|
463
|
-
const cleanup = (): void => {
|
|
464
|
-
if (waiter) run.waiters.delete(waiter);
|
|
465
|
-
signal?.removeEventListener("abort", onAbort);
|
|
466
|
-
};
|
|
467
|
-
const onAbort = (): void => {
|
|
468
|
-
cleanup();
|
|
469
|
-
reject(new CursorAbortError());
|
|
470
|
-
};
|
|
471
|
-
waiter = (): void => {
|
|
472
|
-
cleanup();
|
|
473
|
-
resolve();
|
|
474
|
-
};
|
|
475
|
-
run.waiters.add(waiter);
|
|
476
|
-
if (signal?.aborted) {
|
|
477
|
-
onAbort();
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<void> {
|
|
485
|
-
const eventType = run.pendingEvents[0]?.type;
|
|
486
|
-
if (eventType !== "tool" && eventType !== "bridge-tool") return;
|
|
487
|
-
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function closeCursorNativeThinkingBlock(turn: CursorLiveTurnState): void {
|
|
491
|
-
if (turn.thinkingContentIndex < 0) return;
|
|
492
|
-
const block = turn.partial.content[turn.thinkingContentIndex];
|
|
493
|
-
if (block.type === "thinking") {
|
|
494
|
-
turn.stream.push({
|
|
495
|
-
type: "thinking_end",
|
|
496
|
-
contentIndex: turn.thinkingContentIndex,
|
|
497
|
-
content: block.thinking,
|
|
498
|
-
partial: turn.partial,
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
turn.thinkingContentIndex = -1;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function closeCursorNativeTextBlock(turn: CursorLiveTurnState): string {
|
|
505
|
-
if (turn.textContentIndex < 0) return "";
|
|
506
|
-
const contentIndex = turn.textContentIndex;
|
|
507
|
-
const block = turn.partial.content[contentIndex];
|
|
508
|
-
turn.textContentIndex = -1;
|
|
509
|
-
if (block.type !== "text") return "";
|
|
510
|
-
turn.stream.push({
|
|
511
|
-
type: "text_end",
|
|
512
|
-
contentIndex,
|
|
513
|
-
content: block.text,
|
|
514
|
-
partial: turn.partial,
|
|
515
|
-
});
|
|
516
|
-
return block.text;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function closeCursorNativeTurnBlocks(turn: CursorLiveTurnState): string {
|
|
520
|
-
closeCursorNativeThinkingBlock(turn);
|
|
521
|
-
return closeCursorNativeTextBlock(turn);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function emitCursorNativeThinkingDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
525
|
-
closeCursorNativeTextBlock(turn);
|
|
526
|
-
if (turn.thinkingContentIndex < 0) {
|
|
527
|
-
turn.thinkingContentIndex = turn.partial.content.length;
|
|
528
|
-
turn.partial.content.push({ type: "thinking", thinking: "" });
|
|
529
|
-
turn.stream.push({ type: "thinking_start", contentIndex: turn.thinkingContentIndex, partial: turn.partial });
|
|
530
|
-
}
|
|
531
|
-
const block = turn.partial.content[turn.thinkingContentIndex];
|
|
532
|
-
if (block.type !== "thinking") return;
|
|
533
|
-
block.thinking += delta;
|
|
534
|
-
turn.stream.push({ type: "thinking_delta", contentIndex: turn.thinkingContentIndex, delta, partial: turn.partial });
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function emitCursorNativeTextDelta(turn: CursorLiveTurnState, delta: string): void {
|
|
538
|
-
closeCursorNativeThinkingBlock(turn);
|
|
539
|
-
if (turn.textContentIndex < 0) {
|
|
540
|
-
turn.textContentIndex = turn.partial.content.length;
|
|
541
|
-
turn.partial.content.push({ type: "text", text: "" });
|
|
542
|
-
turn.stream.push({ type: "text_start", contentIndex: turn.textContentIndex, partial: turn.partial });
|
|
543
|
-
}
|
|
544
|
-
const block = turn.partial.content[turn.textContentIndex];
|
|
545
|
-
if (block.type !== "text") return;
|
|
546
|
-
block.text += delta;
|
|
547
|
-
turn.stream.push({ type: "text_delta", contentIndex: turn.textContentIndex, delta, partial: turn.partial });
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function emitCursorLiveQueuedEvent(
|
|
551
|
-
turn: CursorLiveTurnState,
|
|
552
|
-
event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
|
|
553
|
-
run?: CursorLiveRun,
|
|
554
|
-
): void {
|
|
555
|
-
if (event.type === "thinking-delta") {
|
|
556
|
-
emitCursorNativeThinkingDelta(turn, event.text);
|
|
557
|
-
} else if (event.type === "thinking-completed") {
|
|
558
|
-
closeCursorNativeThinkingBlock(turn);
|
|
559
|
-
} else if (event.type === "text-delta") {
|
|
560
|
-
if (run) run.emittedText += event.text;
|
|
561
|
-
emitCursorNativeTextDelta(turn, event.text);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function collectCursorNativeToolBatch(run: CursorLiveRun): CursorNativeToolDisplayItem[] {
|
|
566
|
-
const tools: CursorNativeToolDisplayItem[] = [];
|
|
567
|
-
while (run.pendingEvents[0]?.type === "tool") {
|
|
568
|
-
const event = run.pendingEvents.shift();
|
|
569
|
-
if (event?.type === "tool") tools.push(event.tool);
|
|
570
|
-
}
|
|
571
|
-
return tools;
|
|
572
|
-
}
|
|
573
|
-
|
|
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 {
|
|
609
|
-
// Native replay can split one Cursor run into multiple pi turns; count prompt input once.
|
|
610
|
-
if (run.promptInputTokensReported) return 0;
|
|
611
|
-
run.promptInputTokensReported = true;
|
|
612
|
-
return run.promptInputTokens;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function emitCursorNativeToolUseTurn(
|
|
616
|
-
stream: AssistantMessageEventStream,
|
|
617
|
-
partial: AssistantMessage,
|
|
618
|
-
run: CursorLiveRun,
|
|
619
|
-
tools: CursorNativeToolDisplayItem[],
|
|
620
|
-
outputText: string,
|
|
621
|
-
): void {
|
|
622
|
-
const shouldTerminate = run.done && !run.finalText?.trim() && run.pendingEvents.length === 0;
|
|
623
|
-
for (const tool of tools) {
|
|
624
|
-
const contentIndex = partial.content.length;
|
|
625
|
-
partial.content.push({
|
|
626
|
-
type: "toolCall",
|
|
627
|
-
id: tool.id,
|
|
628
|
-
name: tool.toolName,
|
|
629
|
-
arguments: tool.args,
|
|
630
|
-
});
|
|
631
|
-
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
632
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(tool.args), partial });
|
|
633
|
-
const block = partial.content[contentIndex];
|
|
634
|
-
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
635
|
-
if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
|
|
636
|
-
run.recordedToolDisplayIds.push(tool.id);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
|
|
640
|
-
partial.stopReason = "toolUse";
|
|
641
|
-
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
642
|
-
scheduleCursorNativeRunIdleDispose(run);
|
|
643
|
-
}
|
|
644
|
-
|
|
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> {
|
|
672
|
-
if (run.disposed) return;
|
|
673
|
-
run.disposed = true;
|
|
674
|
-
pendingCursorLiveRuns.delete(run.id);
|
|
675
|
-
clearCursorNativeRunIdleDispose(run);
|
|
676
|
-
run.bridgeRun?.cancel("Cursor live run disposed");
|
|
677
|
-
for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
|
|
678
|
-
run.recordedToolDisplayIds = [];
|
|
679
|
-
run.waiters.clear();
|
|
680
|
-
try {
|
|
681
|
-
await run.bridgeRun?.dispose();
|
|
682
|
-
} catch {
|
|
683
|
-
// bridge disposal failure should not mask the provider result
|
|
684
|
-
}
|
|
685
|
-
try {
|
|
686
|
-
await run.agent[Symbol.asyncDispose]();
|
|
687
|
-
} catch {
|
|
688
|
-
// disposal failure should not mask the provider result
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
async function emitCursorNativeRunNextTurn(
|
|
693
|
-
stream: AssistantMessageEventStream,
|
|
694
|
-
partial: AssistantMessage,
|
|
695
|
-
run: CursorLiveRun,
|
|
696
|
-
signal?: AbortSignal,
|
|
697
|
-
): Promise<void> {
|
|
698
|
-
const turn: CursorLiveTurnState = {
|
|
699
|
-
stream,
|
|
700
|
-
partial,
|
|
701
|
-
thinkingContentIndex: -1,
|
|
702
|
-
textContentIndex: -1,
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
while (true) {
|
|
706
|
-
while (run.pendingEvents.length > 0) {
|
|
707
|
-
const event = run.pendingEvents[0];
|
|
708
|
-
if (event.type === "tool") {
|
|
709
|
-
await settleCursorLiveToolBatch(run);
|
|
710
|
-
if (signal?.aborted) throw new CursorAbortError();
|
|
711
|
-
const outputText = closeCursorNativeTurnBlocks(turn);
|
|
712
|
-
const tools = collectCursorNativeToolBatch(run);
|
|
713
|
-
emitCursorNativeToolUseTurn(stream, partial, run, tools, outputText);
|
|
714
|
-
return;
|
|
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
|
-
}
|
|
724
|
-
run.pendingEvents.shift();
|
|
725
|
-
emitCursorLiveQueuedEvent(turn, event, run);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (run.cancelled) {
|
|
729
|
-
partial.stopReason = "aborted";
|
|
730
|
-
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
731
|
-
await disposeCursorNativeRun(run);
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (run.errorMessage) {
|
|
735
|
-
partial.stopReason = "error";
|
|
736
|
-
partial.errorMessage = run.errorMessage;
|
|
737
|
-
stream.push({ type: "error", reason: "error", error: partial });
|
|
738
|
-
await disposeCursorNativeRun(run);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
if (run.done) {
|
|
742
|
-
let outputText = closeCursorNativeTurnBlocks(turn);
|
|
743
|
-
const finalText = trimAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), run.emittedText);
|
|
744
|
-
if (finalText) {
|
|
745
|
-
outputText += await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
|
|
746
|
-
}
|
|
747
|
-
setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
|
|
748
|
-
partial.stopReason = "stop";
|
|
749
|
-
stream.push({ type: "done", reason: "stop", message: partial });
|
|
750
|
-
await disposeCursorNativeRun(run);
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
await waitForCursorNativeRunProgress(run, signal);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
async function replayPendingCursorLiveRun(
|
|
759
|
-
stream: AssistantMessageEventStream,
|
|
760
|
-
partial: AssistantMessage,
|
|
761
|
-
context: Context,
|
|
762
|
-
signal?: AbortSignal,
|
|
763
|
-
): Promise<boolean> {
|
|
764
|
-
const run = getPendingCursorLiveRun(context);
|
|
765
|
-
if (!run) return false;
|
|
766
|
-
clearCursorNativeRunIdleDispose(run);
|
|
767
|
-
run.bridgeRun?.resolveToolResultsFromContext(context);
|
|
768
|
-
try {
|
|
769
|
-
await emitCursorNativeRunNextTurn(stream, partial, run, signal);
|
|
770
|
-
} catch (error) {
|
|
771
|
-
if (error instanceof CursorAbortError) await disposeCursorNativeRun(run);
|
|
772
|
-
throw error;
|
|
773
|
-
}
|
|
774
|
-
return true;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
132
|
export function streamCursor(
|
|
778
133
|
model: Model<Api>,
|
|
779
134
|
context: Context,
|
|
@@ -786,23 +141,23 @@ export function streamCursor(
|
|
|
786
141
|
let agent: SDKAgent | null = null;
|
|
787
142
|
let activeLiveRun: CursorLiveRun | undefined;
|
|
788
143
|
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
789
|
-
let bridgeRunOwnedByLiveRun = false;
|
|
790
144
|
let liveRunForBridgeQueue: CursorLiveRun | undefined;
|
|
791
145
|
const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
|
|
792
146
|
let resolvedApiKey: string | undefined;
|
|
147
|
+
let sessionAgentScopeKey = "";
|
|
793
148
|
let abortSignal: AbortSignal | undefined;
|
|
794
149
|
let abortListener: (() => void) | undefined;
|
|
795
150
|
let restoreCursorSdkOutputFilter: (() => void) | undefined;
|
|
796
151
|
|
|
797
152
|
try {
|
|
798
153
|
const throwIfAborted = (): void => {
|
|
799
|
-
if (options?.signal?.aborted) throw new
|
|
154
|
+
if (options?.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
800
155
|
};
|
|
801
156
|
|
|
802
157
|
stream.push({ type: "start", partial });
|
|
803
158
|
throwIfAborted();
|
|
804
159
|
|
|
805
|
-
if (await
|
|
160
|
+
if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal)) === "stream_ended") {
|
|
806
161
|
stream.end();
|
|
807
162
|
return;
|
|
808
163
|
}
|
|
@@ -818,389 +173,73 @@ export function streamCursor(
|
|
|
818
173
|
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
819
174
|
const settingSources = resolveCursorSettingSources();
|
|
820
175
|
|
|
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
176
|
installCursorMcpToolTimeoutOverride();
|
|
839
177
|
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
178
|
+
const sessionAgentAcquireParams = {
|
|
179
|
+
apiKey,
|
|
180
|
+
cwd,
|
|
181
|
+
modelSelection: selection,
|
|
182
|
+
settingSources,
|
|
183
|
+
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
184
|
+
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
185
|
+
cursorLiveRuns.queueEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
186
|
+
} else {
|
|
187
|
+
queuedBridgeRequestsBeforeLiveRun.push(request);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
createAgent: (createOptions: Parameters<typeof Agent.create>[0]) =>
|
|
191
|
+
suppressCursorSdkOutput(() => Agent.create(createOptions)),
|
|
192
|
+
};
|
|
193
|
+
let sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
194
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
195
|
+
agent = sessionAgentLease.agent;
|
|
196
|
+
bridgeRun = sessionAgentLease.bridgeRun;
|
|
848
197
|
throwIfAborted();
|
|
849
198
|
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
199
|
+
const promptOptions = getCursorPromptOptions(model);
|
|
200
|
+
let { prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState);
|
|
201
|
+
if (sessionAgentLease.sendState.bootstrapped && bootstrap) {
|
|
202
|
+
await resetSessionCursorAgent(sessionAgentLease.scopeKey);
|
|
203
|
+
sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
204
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
205
|
+
agent = sessionAgentLease.agent;
|
|
206
|
+
bridgeRun = sessionAgentLease.bridgeRun;
|
|
207
|
+
({ prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState));
|
|
208
|
+
}
|
|
209
|
+
const sessionBridgeRun = sessionAgentLease.bridgeRun;
|
|
210
|
+
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
861
211
|
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
862
212
|
const nativeReplayId = createCursorNativeReplayId();
|
|
863
213
|
const textDeltas: string[] = [];
|
|
864
|
-
let nativeToolReplayStarted = false;
|
|
865
214
|
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
866
215
|
const liveRun: CursorLiveRun | undefined = useLiveRun
|
|
867
|
-
? {
|
|
216
|
+
? cursorLiveRuns.start({
|
|
868
217
|
id: useNativeToolReplay ? nativeReplayId : bridgeRun!.id,
|
|
869
218
|
agent,
|
|
870
219
|
bridgeRun,
|
|
220
|
+
sessionBridgeRun,
|
|
221
|
+
sessionAgentScopeKey,
|
|
871
222
|
promptInputTokens,
|
|
872
|
-
promptInputTokensReported: false,
|
|
873
|
-
pendingEvents: [],
|
|
874
223
|
textDeltas,
|
|
875
|
-
|
|
876
|
-
recordedToolDisplayIds: [],
|
|
877
|
-
done: false,
|
|
878
|
-
cancelled: false,
|
|
879
|
-
disposed: false,
|
|
880
|
-
waiters: new Set(),
|
|
881
|
-
}
|
|
224
|
+
})
|
|
882
225
|
: undefined;
|
|
883
226
|
if (liveRun) {
|
|
884
|
-
pendingCursorLiveRuns.set(liveRun.id, liveRun);
|
|
885
227
|
activeLiveRun = liveRun;
|
|
886
228
|
liveRunForBridgeQueue = liveRun;
|
|
887
|
-
bridgeRunOwnedByLiveRun = bridgeRun !== undefined;
|
|
888
229
|
for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
|
|
889
|
-
|
|
230
|
+
cursorLiveRuns.queueEvent(liveRun, { type: "bridge-tool", request });
|
|
890
231
|
}
|
|
891
232
|
}
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const appendLiveTextDelta = (text: string): void => {
|
|
903
|
-
if (textContentIndex < 0) {
|
|
904
|
-
textContentIndex = partial.content.length;
|
|
905
|
-
partial.content.push({ type: "text", text: "" });
|
|
906
|
-
stream.push({ type: "text_start", contentIndex: textContentIndex, partial });
|
|
907
|
-
}
|
|
908
|
-
const block = partial.content[textContentIndex];
|
|
909
|
-
if (block.type !== "text") return;
|
|
910
|
-
block.text += text;
|
|
911
|
-
stream.push({
|
|
912
|
-
type: "text_delta",
|
|
913
|
-
contentIndex: textContentIndex,
|
|
914
|
-
delta: text,
|
|
915
|
-
partial,
|
|
916
|
-
});
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
const appendTraceDelta = (text: string): void => {
|
|
920
|
-
if (activityTraceTruncated) return;
|
|
921
|
-
|
|
922
|
-
let delta = text;
|
|
923
|
-
if (activityTraceChars + delta.length > CURSOR_ACTIVITY_TRACE_MAX_CHARS) {
|
|
924
|
-
const remainingChars = Math.max(CURSOR_ACTIVITY_TRACE_MAX_CHARS - activityTraceChars, 0);
|
|
925
|
-
delta = `${delta.slice(0, remainingChars)}\n[Cursor activity trace truncated]\n`;
|
|
926
|
-
activityTraceTruncated = true;
|
|
927
|
-
}
|
|
928
|
-
if (!delta) return;
|
|
929
|
-
|
|
930
|
-
if (thinkingContentIndex < 0) {
|
|
931
|
-
thinkingContentIndex = partial.content.length;
|
|
932
|
-
partial.content.push({ type: "thinking", thinking: "" });
|
|
933
|
-
stream.push({ type: "thinking_start", contentIndex: thinkingContentIndex, partial });
|
|
934
|
-
}
|
|
935
|
-
const block = partial.content[thinkingContentIndex];
|
|
936
|
-
if (block.type === "thinking") {
|
|
937
|
-
block.thinking += delta;
|
|
938
|
-
activityTraceChars += delta.length;
|
|
939
|
-
stream.push({
|
|
940
|
-
type: "thinking_delta",
|
|
941
|
-
contentIndex: thinkingContentIndex,
|
|
942
|
-
delta,
|
|
943
|
-
partial,
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
const appendTraceLine = (text: string): void => {
|
|
949
|
-
appendTraceDelta(`${text}\n`);
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
const appendTraceBlock = (text: string): void => {
|
|
953
|
-
closeTraceBlock();
|
|
954
|
-
appendTraceDelta(text.endsWith("\n") ? text : `${text}\n`);
|
|
955
|
-
closeTraceBlock();
|
|
956
|
-
};
|
|
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
|
-
|
|
968
|
-
const closeTraceBlock = (): void => {
|
|
969
|
-
if (thinkingContentIndex < 0) return;
|
|
970
|
-
const block = partial.content[thinkingContentIndex];
|
|
971
|
-
if (block.type === "thinking") {
|
|
972
|
-
stream.push({
|
|
973
|
-
type: "thinking_end",
|
|
974
|
-
contentIndex: thinkingContentIndex,
|
|
975
|
-
content: block.thinking,
|
|
976
|
-
partial,
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
thinkingContentIndex = -1;
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
const flushText = (deltas: string[]): string => {
|
|
983
|
-
for (const delta of deltas) appendLiveTextDelta(delta);
|
|
984
|
-
if (textContentIndex < 0) return "";
|
|
985
|
-
const block = partial.content[textContentIndex];
|
|
986
|
-
if (block.type !== "text") return "";
|
|
987
|
-
stream.push({
|
|
988
|
-
type: "text_end",
|
|
989
|
-
contentIndex: textContentIndex,
|
|
990
|
-
content: block.text,
|
|
991
|
-
partial,
|
|
992
|
-
});
|
|
993
|
-
return block.text;
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
const getToolFingerprint = (value: unknown): string => {
|
|
997
|
-
try {
|
|
998
|
-
return JSON.stringify(value);
|
|
999
|
-
} catch {
|
|
1000
|
-
return String(value);
|
|
1001
|
-
}
|
|
1002
|
-
};
|
|
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
|
-
|
|
1067
|
-
const handleCompletedToolCall = (
|
|
1068
|
-
toolCall: unknown,
|
|
1069
|
-
options: { identity?: string; source?: "started" | "fallback" } = {},
|
|
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
|
-
}
|
|
1078
|
-
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
|
|
1079
|
-
const display = buildCursorPiToolDisplay(toolCall, { cwd });
|
|
1080
|
-
const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
|
|
1081
|
-
if (options.identity && completedToolIdentities.has(options.identity)) return;
|
|
1082
|
-
if (options.source === "started") {
|
|
1083
|
-
if (completedFallbackToolFingerprints.has(fingerprint)) return;
|
|
1084
|
-
} else if (completedStartedToolFingerprints.has(fingerprint) || completedFallbackToolFingerprints.has(fingerprint)) {
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
if (options.identity) completedToolIdentities.add(options.identity);
|
|
1088
|
-
if (options.source === "started") {
|
|
1089
|
-
completedStartedToolFingerprints.add(fingerprint);
|
|
1090
|
-
} else {
|
|
1091
|
-
completedFallbackToolFingerprints.add(fingerprint);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
const nativeRenderable = canRenderCursorToolNatively(display.toolName);
|
|
1095
|
-
const route = useNativeToolReplay && nativeRenderable && liveRun ? "native_replay" : "trace";
|
|
1096
|
-
|
|
1097
|
-
if (route === "native_replay" && liveRun) {
|
|
1098
|
-
nativeToolReplayStarted = true;
|
|
1099
|
-
const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
|
|
1100
|
-
queueCursorNativeEvent(liveRun, {
|
|
1101
|
-
type: "tool",
|
|
1102
|
-
tool: {
|
|
1103
|
-
...display,
|
|
1104
|
-
id,
|
|
1105
|
-
args: scrubDisplayValue(display.args, resolvedApiKey) as Record<string, unknown>,
|
|
1106
|
-
result: scrubDisplayValue(display.result, resolvedApiKey) as typeof display.result,
|
|
1107
|
-
},
|
|
1108
|
-
});
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
emitCursorToolTrace(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
|
|
1113
|
-
};
|
|
1114
|
-
|
|
1115
|
-
const onDelta = (args: { update: InteractionUpdate }): void => {
|
|
1116
|
-
const update = args.update;
|
|
1117
|
-
|
|
1118
|
-
if (update.type === "text-delta") {
|
|
1119
|
-
textDeltas.push(update.text);
|
|
1120
|
-
if (liveRun) {
|
|
1121
|
-
queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
|
|
1122
|
-
} else {
|
|
1123
|
-
appendLiveTextDelta(update.text);
|
|
1124
|
-
}
|
|
1125
|
-
} else if (update.type === "thinking-delta") {
|
|
1126
|
-
if (liveRun) {
|
|
1127
|
-
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
|
|
1128
|
-
} else {
|
|
1129
|
-
appendTraceDelta(update.text);
|
|
1130
|
-
}
|
|
1131
|
-
} else if (update.type === "thinking-completed") {
|
|
1132
|
-
if (liveRun) {
|
|
1133
|
-
queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
|
|
1134
|
-
} else {
|
|
1135
|
-
closeTraceBlock();
|
|
1136
|
-
}
|
|
1137
|
-
} else if (update.type === "tool-call-started") {
|
|
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
|
-
}
|
|
1144
|
-
} else if (update.type === "tool-call-completed") {
|
|
1145
|
-
const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
|
|
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, {
|
|
1155
|
-
identity,
|
|
1156
|
-
source: identity ? "started" : "fallback",
|
|
1157
|
-
});
|
|
1158
|
-
} else if (update.type === "shell-output-delta") {
|
|
1159
|
-
const delta = getCursorShellOutputDelta(update);
|
|
1160
|
-
if (delta) appendShellOutputDelta(delta);
|
|
1161
|
-
} else if (update.type === "summary") {
|
|
1162
|
-
const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
|
|
1163
|
-
if (liveRun) {
|
|
1164
|
-
queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
|
|
1165
|
-
} else {
|
|
1166
|
-
appendTraceDelta(summary);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
// Cursor turn-ended usage is intentionally not copied into pi usage: the SDK reports
|
|
1170
|
-
// cumulative internal agent/tool/cache tokens, not the replayable pi prompt context.
|
|
1171
|
-
// partial-tool-call, summary-started, summary-completed, turn-ended,
|
|
1172
|
-
// token-delta, step-* are intentionally not surfaced.
|
|
1173
|
-
};
|
|
1174
|
-
|
|
1175
|
-
const onStep = (args: { step: unknown }): void => {
|
|
1176
|
-
const stepType = getObjectField(args.step, "type");
|
|
1177
|
-
const step = getObjectField(args.step, "message") ? args.step : undefined;
|
|
1178
|
-
const rawStepToolCall = getObjectField(step, "message");
|
|
1179
|
-
if (stepType !== "toolCall") return;
|
|
1180
|
-
const toolCall = rawStepToolCall;
|
|
1181
|
-
const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
|
|
1182
|
-
if (toolCall) {
|
|
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,
|
|
1200
|
-
});
|
|
1201
|
-
if (matchedStartedCallId && matchedStartedCallId !== stepId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
|
|
1202
|
-
}
|
|
1203
|
-
};
|
|
233
|
+
const turnCoordinator = new CursorSdkTurnCoordinator({
|
|
234
|
+
stream,
|
|
235
|
+
partial,
|
|
236
|
+
cwd,
|
|
237
|
+
resolvedApiKey,
|
|
238
|
+
liveRun,
|
|
239
|
+
useNativeToolReplay,
|
|
240
|
+
nativeReplayId,
|
|
241
|
+
textDeltas,
|
|
242
|
+
});
|
|
1204
243
|
|
|
1205
244
|
// Handle abort signal
|
|
1206
245
|
let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
|
|
@@ -1216,11 +255,15 @@ export function streamCursor(
|
|
|
1216
255
|
throwIfAborted();
|
|
1217
256
|
run = await agent.send(
|
|
1218
257
|
{ text: prompt.text, images: prompt.images.length > 0 ? prompt.images : undefined },
|
|
1219
|
-
{
|
|
258
|
+
{
|
|
259
|
+
onDelta: (args) => turnCoordinator.handleDelta(args.update),
|
|
260
|
+
onStep: (args) => turnCoordinator.handleStep(args.step),
|
|
261
|
+
},
|
|
1220
262
|
);
|
|
263
|
+
if (liveRun) cursorLiveRuns.attachSdkRun(liveRun, run);
|
|
1221
264
|
if (options?.signal?.aborted) {
|
|
1222
265
|
await run.cancel().catch(() => {});
|
|
1223
|
-
throw new
|
|
266
|
+
throw new CursorLiveRunAbortError();
|
|
1224
267
|
}
|
|
1225
268
|
|
|
1226
269
|
if (liveRun) {
|
|
@@ -1228,29 +271,35 @@ export function streamCursor(
|
|
|
1228
271
|
.wait()
|
|
1229
272
|
.then(async (result) => {
|
|
1230
273
|
if (liveRun.disposed) return;
|
|
1231
|
-
discardIncompleteStartedToolCalls();
|
|
274
|
+
turnCoordinator.discardIncompleteStartedToolCalls();
|
|
1232
275
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
1233
276
|
if (liveRun.disposed) return;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
277
|
+
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
278
|
+
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
279
|
+
cursorLiveRuns.markFinished(
|
|
280
|
+
liveRun,
|
|
281
|
+
selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, turnCoordinator.planTextCandidate),
|
|
282
|
+
);
|
|
283
|
+
} else if (result.status === "cancelled" || options?.signal?.aborted) {
|
|
284
|
+
cursorLiveRuns.markCancelled(liveRun);
|
|
285
|
+
} else {
|
|
286
|
+
cursorLiveRuns.markError(liveRun, sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey));
|
|
287
|
+
}
|
|
1239
288
|
})
|
|
1240
|
-
.catch((error: unknown) => {
|
|
289
|
+
.catch(async (error: unknown) => {
|
|
1241
290
|
if (liveRun.disposed) return;
|
|
1242
|
-
liveRun
|
|
1243
|
-
notifyCursorNativeRun(liveRun);
|
|
1244
|
-
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
291
|
+
cursorLiveRuns.markError(liveRun, sanitizeError(error, resolvedApiKey ?? options?.apiKey));
|
|
1245
292
|
});
|
|
1246
293
|
|
|
1247
294
|
try {
|
|
1248
|
-
await
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
295
|
+
await cursorLiveRuns.withRunLease(liveRun, options?.signal, async () => {
|
|
296
|
+
await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
|
|
297
|
+
await settleCursorLiveToolBatch(liveRun);
|
|
298
|
+
turnCoordinator.closeTraceBlock();
|
|
299
|
+
await drainCursorLiveRunTurn(stream, partial, model, context, liveRun, 0, { mode: "emit", signal: options?.signal });
|
|
300
|
+
});
|
|
1252
301
|
} catch (error) {
|
|
1253
|
-
if (error instanceof
|
|
302
|
+
if (error instanceof CursorLiveRunAbortError) await cursorLiveRuns.release(liveRun);
|
|
1254
303
|
throw error;
|
|
1255
304
|
}
|
|
1256
305
|
agent = null;
|
|
@@ -1258,25 +307,35 @@ export function streamCursor(
|
|
|
1258
307
|
}
|
|
1259
308
|
|
|
1260
309
|
const result = await run.wait();
|
|
1261
|
-
discardIncompleteStartedToolCalls();
|
|
310
|
+
turnCoordinator.discardIncompleteStartedToolCalls();
|
|
1262
311
|
await cacheSdkContextWindow(agent.agentId, model.id);
|
|
1263
312
|
|
|
1264
313
|
// Close any open thinking/activity trace, then use the final run result only when
|
|
1265
314
|
// Cursor did not stream text deltas.
|
|
1266
|
-
closeTraceBlock();
|
|
315
|
+
turnCoordinator.closeTraceBlock();
|
|
1267
316
|
|
|
1268
317
|
if (result.status === "cancelled") {
|
|
318
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1269
319
|
partial.stopReason = "aborted";
|
|
1270
320
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
321
|
+
} else if (result.status === "error") {
|
|
322
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
323
|
+
partial.stopReason = "error";
|
|
324
|
+
partial.errorMessage = sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey);
|
|
325
|
+
stream.push({ type: "error", reason: "error", error: partial });
|
|
1271
326
|
} else {
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
|
|
327
|
+
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
328
|
+
const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""), turnCoordinator.planTextCandidate, {
|
|
329
|
+
allowPartialPrefix: true,
|
|
330
|
+
});
|
|
331
|
+
turnCoordinator.flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
|
|
332
|
+
applyCursorApproximateUsage(partial, model, context, promptInputTokens);
|
|
1275
333
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
1276
334
|
}
|
|
1277
335
|
} catch (error) {
|
|
1278
|
-
if (activeLiveRun && !activeLiveRun.disposed) await
|
|
1279
|
-
|
|
336
|
+
if (activeLiveRun && !activeLiveRun.disposed) await cursorLiveRuns.release(activeLiveRun);
|
|
337
|
+
else await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
338
|
+
if (error instanceof CursorLiveRunAbortError) {
|
|
1280
339
|
partial.stopReason = "aborted";
|
|
1281
340
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
1282
341
|
} else {
|
|
@@ -1286,28 +345,10 @@ export function streamCursor(
|
|
|
1286
345
|
}
|
|
1287
346
|
} finally {
|
|
1288
347
|
restoreCursorSdkOutputFilter?.();
|
|
1289
|
-
if (activeLiveRun?.disposed) agent = null;
|
|
1290
348
|
|
|
1291
349
|
if (abortSignal && abortListener) {
|
|
1292
350
|
abortSignal.removeEventListener("abort", abortListener);
|
|
1293
351
|
}
|
|
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
|
-
|
|
1303
|
-
if (agent) {
|
|
1304
|
-
try {
|
|
1305
|
-
await agent[Symbol.asyncDispose]();
|
|
1306
|
-
} catch {
|
|
1307
|
-
// disposal failure should not mask original error
|
|
1308
|
-
}
|
|
1309
|
-
agent = null;
|
|
1310
|
-
}
|
|
1311
352
|
}
|
|
1312
353
|
|
|
1313
354
|
stream.end();
|
|
@@ -1318,11 +359,11 @@ export function streamCursor(
|
|
|
1318
359
|
|
|
1319
360
|
export const __testUtils = {
|
|
1320
361
|
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
1321
|
-
pendingCursorNativeRunCount:
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
362
|
+
pendingCursorNativeRunCount: cursorLiveRuns.count,
|
|
363
|
+
getPendingCursorLiveRun,
|
|
364
|
+
getActiveCursorLiveRunForScope: cursorLiveRuns.getActiveForScope,
|
|
365
|
+
hasTrailingUserMessagesAfterToolResults,
|
|
366
|
+
setCursorNativeReplayIdleDisposeMs,
|
|
367
|
+
resetCursorNativeReplayIdleDisposeMs,
|
|
368
|
+
resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
|
|
1328
369
|
};
|