pi-cursor-sdk 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +38 -1
- package/docs/cursor-live-smoke-checklist.md +22 -2
- package/docs/cursor-model-ux-spec.md +5 -4
- package/docs/cursor-native-tool-replay.md +96 -2
- package/docs/cursor-testing-lessons.md +428 -0
- package/package.json +11 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/validate-smoke-jsonl.mjs +86 -7
- package/src/context.ts +45 -32
- package/src/cursor-agent-message-web-tools.ts +172 -0
- package/src/cursor-agents-context.ts +176 -0
- package/src/cursor-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-incomplete-tool-visibility.ts +118 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +14 -7
- package/src/cursor-native-tool-display-replay.ts +63 -5
- package/src/cursor-native-tool-display-tools.ts +20 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
- package/src/cursor-pi-tool-bridge-run.ts +16 -1
- package/src/cursor-pi-tool-bridge-types.ts +3 -0
- package/src/cursor-provider-errors.ts +96 -0
- package/src/cursor-provider-live-run-drain.ts +208 -63
- package/src/cursor-provider-turn-coordinator.ts +217 -47
- package/src/cursor-provider.ts +275 -83
- package/src/cursor-question-tool.ts +10 -5
- package/src/cursor-sdk-abort-error-guard.ts +109 -0
- package/src/cursor-sdk-event-debug-constants.ts +40 -0
- package/src/cursor-sdk-event-debug-session.ts +163 -0
- package/src/cursor-sdk-event-debug.ts +597 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +25 -3
- package/src/cursor-session-send-policy.ts +43 -0
- package/src/cursor-setting-sources.ts +29 -0
- package/src/cursor-state.ts +1 -5
- package/src/cursor-tool-lifecycle.ts +111 -0
- package/src/cursor-tool-names.ts +12 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +113 -14
- package/src/cursor-transcript-utils.ts +12 -0
- package/src/cursor-web-tool-activity.ts +84 -0
- package/src/index.ts +4 -1
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
createCursorReplayOnlyToolDefinition,
|
|
22
22
|
renderCursorReplayResult,
|
|
23
23
|
renderNativeLookingCursorFileMutationCall,
|
|
24
|
+
renderNativeLookingCursorReadReplayResult,
|
|
24
25
|
} from "./cursor-native-tool-display-replay.js";
|
|
25
26
|
import {
|
|
26
27
|
consumeCursorNativeToolDisplay,
|
|
@@ -65,6 +66,20 @@ export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
|
|
|
65
66
|
return getCurrentDefinition().execute(toolCallId, params, signal, onUpdate, ctx);
|
|
66
67
|
},
|
|
67
68
|
renderCall(args, theme, context) {
|
|
69
|
+
if (definition.name === "read" && isCursorReplayToolCallId(context.toolCallId)) {
|
|
70
|
+
const currentRenderCall = getCurrentDefinition().renderCall;
|
|
71
|
+
const rendered = currentRenderCall ? currentRenderCall(args, theme, context) : new Text("", 0, 0);
|
|
72
|
+
if ((args as Record<string, unknown>).localReadPreview === true && !context.expanded) {
|
|
73
|
+
const baseText = rendered.render(120).join("\n").trimEnd();
|
|
74
|
+
const labeled = `${baseText}${theme.fg("muted", " · local file preview")}`;
|
|
75
|
+
if (rendered instanceof Text) {
|
|
76
|
+
rendered.setText(labeled);
|
|
77
|
+
return rendered;
|
|
78
|
+
}
|
|
79
|
+
return new Text(labeled, 0, 0);
|
|
80
|
+
}
|
|
81
|
+
return rendered;
|
|
82
|
+
}
|
|
68
83
|
if (isCursorFileMutationToolName(definition.name) && isCursorReplayToolCallId(context.toolCallId)) {
|
|
69
84
|
return renderNativeLookingCursorFileMutationCall(definition.name, args as Record<string, unknown>, theme, context.isPartial);
|
|
70
85
|
}
|
|
@@ -76,6 +91,11 @@ export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
|
|
|
76
91
|
if (isCursorFileMutationToolName(definition.name) && details?.cursorToolName === definition.name) {
|
|
77
92
|
return renderCursorReplayResult(result, options, theme, context, context.isError);
|
|
78
93
|
}
|
|
94
|
+
if (definition.name === "read" && isCursorReplayToolCallId(context.toolCallId)) {
|
|
95
|
+
return renderNativeLookingCursorReadReplayResult(result, options, theme, context, () =>
|
|
96
|
+
getCurrentDefinition().renderResult?.(result, options, theme, context),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
79
99
|
const currentRenderResult = getCurrentDefinition().renderResult;
|
|
80
100
|
return currentRenderResult ? currentRenderResult(result, options, theme, context) : new Text("", 0, 0);
|
|
81
101
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
|
|
2
2
|
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
3
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
3
4
|
|
|
4
5
|
export const CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG";
|
|
5
6
|
export const CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX = "[pi-cursor-sdk:bridge]";
|
|
@@ -169,7 +170,16 @@ export function serializeCursorPiToolBridgeDiagnostic(event: CursorPiToolBridgeD
|
|
|
169
170
|
return assertNeverDiagnosticEvent(event);
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
export function writeCursorPiToolBridgeDiagnostic(
|
|
173
|
+
export function writeCursorPiToolBridgeDiagnostic(
|
|
174
|
+
env: Record<string, string | undefined>,
|
|
175
|
+
event: CursorPiToolBridgeDiagnosticEvent,
|
|
176
|
+
debugRecorder?: CursorSdkEventDebugRecorder,
|
|
177
|
+
): void {
|
|
178
|
+
try {
|
|
179
|
+
debugRecorder?.recordBridgeDiagnostic(event);
|
|
180
|
+
} catch {
|
|
181
|
+
// Diagnostics must never affect bridge execution.
|
|
182
|
+
}
|
|
173
183
|
if (!resolveCursorPiToolBridgeDebugEnabled(env)) return;
|
|
174
184
|
try {
|
|
175
185
|
process.stderr.write(`${CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX} ${JSON.stringify(serializeCursorPiToolBridgeDiagnostic(event))}\n`);
|
|
@@ -67,6 +67,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
67
67
|
private readonly pendingByBridgeCallId = new Map<string, PendingBridgeCall>();
|
|
68
68
|
private readonly pendingByCursorMcpCallId = new Map<string, PendingBridgeCall>();
|
|
69
69
|
private onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
70
|
+
private debugRecorder: CursorPiToolBridgeRunOptions["debugRecorder"];
|
|
70
71
|
private liveRunHandlerDetached = false;
|
|
71
72
|
private mcpServer?: McpProtocolServer;
|
|
72
73
|
private mcpTransport?: StreamableHTTPServerTransport;
|
|
@@ -85,6 +86,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
85
86
|
this.snapshot = snapshot;
|
|
86
87
|
this.enabled = enabled;
|
|
87
88
|
this.onToolRequest = options.onToolRequest;
|
|
89
|
+
this.debugRecorder = options.debugRecorder;
|
|
88
90
|
this.id = `cursor-pi-bridge-run-${randomUUID()}`;
|
|
89
91
|
this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
|
|
90
92
|
this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
|
|
@@ -146,6 +148,10 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
setDebugRecorder(recorder?: CursorPiToolBridgeRunOptions["debugRecorder"]): void {
|
|
152
|
+
this.debugRecorder = recorder;
|
|
153
|
+
}
|
|
154
|
+
|
|
149
155
|
resolveToolResults(toolResults: readonly ToolResultMessage[]): void {
|
|
150
156
|
for (const toolResult of toolResults) {
|
|
151
157
|
const pending = this.pendingByPiToolCallId.get(toolResult.toolCallId);
|
|
@@ -290,9 +296,11 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
290
296
|
}
|
|
291
297
|
this.queuedRequests.push(request);
|
|
292
298
|
this.emitRequestQueuedDiagnostic(request);
|
|
299
|
+
this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
|
|
293
300
|
return;
|
|
294
301
|
}
|
|
295
302
|
this.emitRequestQueuedDiagnostic(request);
|
|
303
|
+
this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
|
|
296
304
|
this.dispatchPendingToolRequest(pending, this.onToolRequest);
|
|
297
305
|
});
|
|
298
306
|
}
|
|
@@ -321,6 +329,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
321
329
|
pending.settled = true;
|
|
322
330
|
this.removePending(pending);
|
|
323
331
|
this.emitRequestResolvedDiagnostic(pending.request, result.isError === true);
|
|
332
|
+
this.debugRecorder?.recordBridgeRaw({ kind: "resolved", request: pending.request, result });
|
|
324
333
|
pending.resolve(result);
|
|
325
334
|
}
|
|
326
335
|
|
|
@@ -329,6 +338,12 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
329
338
|
pending.settled = true;
|
|
330
339
|
this.removePending(pending);
|
|
331
340
|
this.emitRequestRejectedDiagnostic(pending.request, kind);
|
|
341
|
+
this.debugRecorder?.recordBridgeRaw({
|
|
342
|
+
kind: "rejected",
|
|
343
|
+
request: pending.request,
|
|
344
|
+
error: error.message,
|
|
345
|
+
rejectionKind: kind,
|
|
346
|
+
});
|
|
332
347
|
pending.reject(error);
|
|
333
348
|
}
|
|
334
349
|
|
|
@@ -366,7 +381,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
366
381
|
}
|
|
367
382
|
|
|
368
383
|
private emitDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): void {
|
|
369
|
-
writeCursorPiToolBridgeDiagnostic(this.env, event);
|
|
384
|
+
writeCursorPiToolBridgeDiagnostic(this.env, event, this.debugRecorder);
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
private pendingCount(): number {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServerConfig } from "@cursor/sdk";
|
|
2
2
|
import type { Context, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
3
4
|
import type {
|
|
4
5
|
ExtensionAPI,
|
|
5
6
|
ExtensionHandler,
|
|
@@ -64,6 +65,7 @@ export interface CursorPiToolBridgeRun {
|
|
|
64
65
|
hasPendingPiToolCallId(piToolCallId: string): boolean;
|
|
65
66
|
isBridgeMcpToolCall(toolCall: unknown): boolean;
|
|
66
67
|
setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void;
|
|
68
|
+
setDebugRecorder(recorder?: CursorSdkEventDebugRecorder): void;
|
|
67
69
|
cancel(reason: string): void;
|
|
68
70
|
dispose(): Promise<void>;
|
|
69
71
|
}
|
|
@@ -77,4 +79,5 @@ export interface CursorPiToolBridge {
|
|
|
77
79
|
|
|
78
80
|
export interface CursorPiToolBridgeRunOptions {
|
|
79
81
|
onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
82
|
+
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
80
83
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { RunResult } from "@cursor/sdk";
|
|
2
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
3
|
+
|
|
4
|
+
export const MISSING_CURSOR_API_KEY_MESSAGE =
|
|
5
|
+
"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.";
|
|
6
|
+
const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
7
|
+
"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.";
|
|
8
|
+
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
9
|
+
"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.";
|
|
10
|
+
const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
|
|
11
|
+
"Cursor SDK request timed out during network I/O. Check your connection and retry; if this keeps happening, try again later or verify Cursor service availability.";
|
|
12
|
+
|
|
13
|
+
const GENERIC_CURSOR_RUN_FAILURE_TEXT = "cursor sdk run failed";
|
|
14
|
+
|
|
15
|
+
export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "status" | "durationMs" | "model" | "result">;
|
|
16
|
+
|
|
17
|
+
function isGenericErrorMessage(message: string): boolean {
|
|
18
|
+
const normalized = message.trim().toLowerCase();
|
|
19
|
+
return normalized === "" || normalized === "error" || normalized === "unknown error";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isKnownGenericRunFailureText(message: string): boolean {
|
|
23
|
+
const normalized = message.trim().toLowerCase();
|
|
24
|
+
return normalized === "" || normalized === GENERIC_CURSOR_RUN_FAILURE_TEXT || isGenericErrorMessage(normalized);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isLikelyAuthError(message: string): boolean {
|
|
28
|
+
return /\b(unauthorized|unauthorised|forbidden|invalid api key|invalid key|authentication|auth|401|403)\b/i.test(message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isLikelyNetworkTimeout(message: string): boolean {
|
|
32
|
+
return (
|
|
33
|
+
/\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN)\b/i.test(message) ||
|
|
34
|
+
/\bConnectError\b.*\b(unavailable|deadline|timeout|timed out)\b/i.test(message) ||
|
|
35
|
+
/\bread ETIMEDOUT\b/i.test(message)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function shortRunId(runId: string): string {
|
|
40
|
+
const trimmed = runId.trim();
|
|
41
|
+
if (trimmed.length <= 12) return trimmed;
|
|
42
|
+
return `${trimmed.slice(0, 8)}…`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatCursorSdkRunFailureDetail(result: CursorSdkRunFailureSource, runResult?: string): string {
|
|
46
|
+
const fromWait = result.result?.trim();
|
|
47
|
+
if (fromWait && !isKnownGenericRunFailureText(fromWait)) {
|
|
48
|
+
return fromWait;
|
|
49
|
+
}
|
|
50
|
+
const fromRun = runResult?.trim();
|
|
51
|
+
if (fromRun && !isKnownGenericRunFailureText(fromRun)) {
|
|
52
|
+
return fromRun;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parts = ["Cursor SDK run failed"];
|
|
56
|
+
if (result.model?.id) parts.push(`model ${result.model.id}`);
|
|
57
|
+
parts.push(`run ${shortRunId(result.id)}`);
|
|
58
|
+
if (typeof result.durationMs === "number") parts.push(`${result.durationMs}ms`);
|
|
59
|
+
return parts.join(" · ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type CursorSdkAbortCause = "user_interrupt" | "sdk_cancelled" | "live_run_disposed" | "unknown";
|
|
63
|
+
|
|
64
|
+
export function formatCursorSdkAbortMessage(cause: CursorSdkAbortCause): string {
|
|
65
|
+
switch (cause) {
|
|
66
|
+
case "user_interrupt":
|
|
67
|
+
return "Cancelled: prompt interrupted.";
|
|
68
|
+
case "sdk_cancelled":
|
|
69
|
+
return "Cancelled: Cursor SDK run was cancelled.";
|
|
70
|
+
case "live_run_disposed":
|
|
71
|
+
return "Cancelled: Cursor SDK live run ended before completion.";
|
|
72
|
+
case "unknown":
|
|
73
|
+
return "Cancelled: Cursor SDK run aborted.";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveCursorSdkAbortCause(options: {
|
|
78
|
+
signalAborted?: boolean;
|
|
79
|
+
sdkStatusCancelled?: boolean;
|
|
80
|
+
liveRunDisposed?: boolean;
|
|
81
|
+
}): CursorSdkAbortCause {
|
|
82
|
+
if (options.signalAborted) return "user_interrupt";
|
|
83
|
+
if (options.sdkStatusCancelled) return "sdk_cancelled";
|
|
84
|
+
if (options.liveRunDisposed) return "live_run_disposed";
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function sanitizeCursorProviderError(error: unknown, apiKey?: string): string {
|
|
89
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
90
|
+
if (message === MISSING_CURSOR_API_KEY_MESSAGE) return MISSING_CURSOR_API_KEY_MESSAGE;
|
|
91
|
+
const scrubbed = scrubSensitiveText(message, apiKey).trim();
|
|
92
|
+
if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
93
|
+
if (isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
|
|
94
|
+
if (isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
|
|
95
|
+
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
96
|
+
}
|
|
@@ -23,6 +23,10 @@ import { resetSessionCursorAgent } from "./cursor-session-agent.js";
|
|
|
23
23
|
import { applyCursorApproximateUsage } from "./cursor-usage-accounting.js";
|
|
24
24
|
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
25
25
|
import { hasUsableText } from "./cursor-record-utils.js";
|
|
26
|
+
import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
|
|
27
|
+
import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
|
|
28
|
+
import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
|
|
29
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
26
30
|
|
|
27
31
|
export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
28
32
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
@@ -101,6 +105,37 @@ export async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<voi
|
|
|
101
105
|
await scheduler.wait(75);
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
export function flushPendingCursorLiveRunTraceEventsToStream(
|
|
109
|
+
stream: AssistantMessageEventStream,
|
|
110
|
+
partial: AssistantMessage,
|
|
111
|
+
run: CursorLiveRun,
|
|
112
|
+
options?: { includeTracesBehindQueuedTools?: boolean },
|
|
113
|
+
): void {
|
|
114
|
+
if (run.disposed) return;
|
|
115
|
+
const turn: CursorLiveTurnState = {
|
|
116
|
+
emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
117
|
+
emittedText: "",
|
|
118
|
+
};
|
|
119
|
+
while (true) {
|
|
120
|
+
const event = cursorLiveRuns.peekEvent(run);
|
|
121
|
+
if (!event || event.type === "tool" || event.type === "bridge-tool") break;
|
|
122
|
+
cursorLiveRuns.shiftEvent(run);
|
|
123
|
+
emitCursorLiveQueuedEvent(turn, event, run);
|
|
124
|
+
}
|
|
125
|
+
if (options?.includeTracesBehindQueuedTools && run.pendingEvents.length > 0) {
|
|
126
|
+
const preserved: CursorLiveQueuedEvent[] = [];
|
|
127
|
+
for (const event of run.pendingEvents) {
|
|
128
|
+
if (event.type === "tool" || event.type === "bridge-tool") {
|
|
129
|
+
preserved.push(event);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
emitCursorLiveQueuedEvent(turn, event, run);
|
|
133
|
+
}
|
|
134
|
+
run.pendingEvents = preserved;
|
|
135
|
+
}
|
|
136
|
+
turn.emitter.closeAll();
|
|
137
|
+
}
|
|
138
|
+
|
|
104
139
|
function emitCursorLiveQueuedEvent(
|
|
105
140
|
turn: CursorLiveTurnState,
|
|
106
141
|
event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
|
|
@@ -176,6 +211,7 @@ function emitCursorNativeToolUseTurn(
|
|
|
176
211
|
run: CursorLiveRun,
|
|
177
212
|
toolResultInputTokens: number,
|
|
178
213
|
tools: CursorNativeToolDisplayItem[],
|
|
214
|
+
debugRecorder?: CursorSdkEventDebugRecorder,
|
|
179
215
|
): void {
|
|
180
216
|
const shouldTerminate = run.done && !run.finalText?.trim() && !cursorLiveRuns.peekEvent(run);
|
|
181
217
|
for (const tool of tools) {
|
|
@@ -192,6 +228,11 @@ function emitCursorNativeToolUseTurn(
|
|
|
192
228
|
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
193
229
|
if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
|
|
194
230
|
run.recordedToolDisplayIds.push(tool.id);
|
|
231
|
+
debugRecorder?.recordDrainEvent("native_tool_display_recorded", {
|
|
232
|
+
toolId: tool.id,
|
|
233
|
+
toolName: tool.toolName,
|
|
234
|
+
terminate: shouldTerminate,
|
|
235
|
+
});
|
|
195
236
|
}
|
|
196
237
|
}
|
|
197
238
|
applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
|
|
@@ -200,6 +241,23 @@ function emitCursorNativeToolUseTurn(
|
|
|
200
241
|
cursorLiveRuns.requestIdleDispose(run);
|
|
201
242
|
}
|
|
202
243
|
|
|
244
|
+
function emitInactiveCursorReplayTrace(
|
|
245
|
+
turn: CursorLiveTurnState,
|
|
246
|
+
tools: CursorNativeToolDisplayItem[],
|
|
247
|
+
debugRecorder?: CursorSdkEventDebugRecorder,
|
|
248
|
+
): void {
|
|
249
|
+
if (tools.length === 0) return;
|
|
250
|
+
for (const tool of tools) {
|
|
251
|
+
const traceText = formatInactiveCursorReplayTrace(tool);
|
|
252
|
+
debugRecorder?.recordDrainEvent("inactive_replay_trace", {
|
|
253
|
+
toolId: tool.id,
|
|
254
|
+
toolName: tool.toolName,
|
|
255
|
+
traceText,
|
|
256
|
+
});
|
|
257
|
+
turn.emitter.appendThinkingBlock(traceText);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
203
261
|
function emitCursorBridgeToolUseTurn(
|
|
204
262
|
stream: AssistantMessageEventStream,
|
|
205
263
|
partial: AssistantMessage,
|
|
@@ -229,24 +287,31 @@ function emitCursorBridgeToolUseTurn(
|
|
|
229
287
|
}
|
|
230
288
|
|
|
231
289
|
async function emitCursorLiveRunPendingToolUseTurn(
|
|
290
|
+
turn: CursorLiveTurnState,
|
|
232
291
|
stream: AssistantMessageEventStream,
|
|
233
292
|
partial: AssistantMessage,
|
|
234
293
|
model: Model<Api>,
|
|
235
294
|
context: Context,
|
|
236
295
|
run: CursorLiveRun,
|
|
237
296
|
toolResultInputTokens: number,
|
|
238
|
-
signal?: AbortSignal,
|
|
239
|
-
|
|
240
|
-
|
|
297
|
+
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
|
|
298
|
+
): Promise<"tool_use" | "handled" | undefined> {
|
|
299
|
+
const debugRecorder = options.debugRecorder ?? run.debugRecorder;
|
|
241
300
|
const eventType = cursorLiveRuns.peekEvent(run)?.type;
|
|
242
301
|
if (eventType !== "tool" && eventType !== "bridge-tool") return undefined;
|
|
243
302
|
await settleCursorLiveToolBatch(run);
|
|
244
|
-
if (signal?.aborted) throw new CursorLiveRunAbortError();
|
|
245
|
-
beforeEmit?.();
|
|
303
|
+
if (options.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
246
304
|
if (eventType === "tool") {
|
|
247
|
-
const
|
|
248
|
-
|
|
305
|
+
const { active, inactive } = partitionNativeToolsByActiveContext(context, cursorLiveRuns.collectNativeToolBatch(run));
|
|
306
|
+
if (options.mode === "emit") emitInactiveCursorReplayTrace(turn, inactive, debugRecorder);
|
|
307
|
+
if (active.length === 0) {
|
|
308
|
+
// Inactive-only batch: trace was emitted above; do not emit toolUse.
|
|
309
|
+
return "handled";
|
|
310
|
+
}
|
|
311
|
+
if (options.mode === "emit") turn.emitter.closeAll();
|
|
312
|
+
emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active, debugRecorder);
|
|
249
313
|
} else {
|
|
314
|
+
if (options.mode === "emit") turn.emitter.closeAll();
|
|
250
315
|
const requests = cursorLiveRuns.collectBridgeToolBatch(run);
|
|
251
316
|
emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
|
|
252
317
|
}
|
|
@@ -260,72 +325,123 @@ export async function drainCursorLiveRunTurn(
|
|
|
260
325
|
context: Context,
|
|
261
326
|
run: CursorLiveRun,
|
|
262
327
|
toolResultInputTokens: number,
|
|
263
|
-
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal },
|
|
328
|
+
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
|
|
264
329
|
): Promise<CursorLiveRunDrainOutcome> {
|
|
330
|
+
const debugRecorder = options.debugRecorder ?? run.debugRecorder;
|
|
331
|
+
debugRecorder?.recordDrainEvent("turn_start", {
|
|
332
|
+
mode: options.mode,
|
|
333
|
+
runId: run.id,
|
|
334
|
+
pendingEventCount: run.pendingEvents.length,
|
|
335
|
+
done: run.done,
|
|
336
|
+
});
|
|
337
|
+
let outcome: CursorLiveRunDrainOutcome | undefined;
|
|
338
|
+
let outcomeDetails: Record<string, unknown> = {};
|
|
265
339
|
const turn: CursorLiveTurnState = {
|
|
266
340
|
emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
267
341
|
emittedText: "",
|
|
268
342
|
};
|
|
269
343
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
344
|
+
try {
|
|
345
|
+
while (true) {
|
|
346
|
+
if (options.mode === "chain_user_input" && cursorLiveRuns.isReady(run)) {
|
|
347
|
+
await cursorLiveRuns.release(run);
|
|
348
|
+
outcome = "chain_user_input";
|
|
349
|
+
return outcome;
|
|
350
|
+
}
|
|
275
351
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
352
|
+
while (cursorLiveRuns.peekEvent(run)) {
|
|
353
|
+
const toolUse = await emitCursorLiveRunPendingToolUseTurn(
|
|
354
|
+
turn,
|
|
355
|
+
stream,
|
|
356
|
+
partial,
|
|
357
|
+
model,
|
|
358
|
+
context,
|
|
359
|
+
run,
|
|
360
|
+
toolResultInputTokens,
|
|
361
|
+
options,
|
|
362
|
+
);
|
|
363
|
+
if (toolUse === "tool_use") {
|
|
364
|
+
outcome = "tool_use";
|
|
365
|
+
return outcome;
|
|
366
|
+
}
|
|
367
|
+
if (toolUse === "handled") continue;
|
|
368
|
+
const event = cursorLiveRuns.shiftEvent(run);
|
|
369
|
+
if (!event || event.type === "tool" || event.type === "bridge-tool") continue;
|
|
370
|
+
if (options.mode === "emit") emitCursorLiveQueuedEvent(turn, event, run);
|
|
371
|
+
}
|
|
292
372
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
stream.push({ type: "error", reason: "error", error: partial });
|
|
308
|
-
await cursorLiveRuns.release(run);
|
|
309
|
-
return "error";
|
|
310
|
-
}
|
|
311
|
-
if (run.done) {
|
|
312
|
-
if (options.mode === "chain_user_input") {
|
|
373
|
+
if (run.disposed) {
|
|
374
|
+
partial.stopReason = "aborted";
|
|
375
|
+
partial.errorMessage = formatCursorSdkAbortMessage(
|
|
376
|
+
resolveCursorSdkAbortCause({ liveRunDisposed: true }),
|
|
377
|
+
);
|
|
378
|
+
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
379
|
+
outcome = "aborted";
|
|
380
|
+
outcomeDetails = { reason: "disposed" };
|
|
381
|
+
return outcome;
|
|
382
|
+
}
|
|
383
|
+
if (run.cancelled) {
|
|
384
|
+
partial.stopReason = "aborted";
|
|
385
|
+
if (run.abortMessage) partial.errorMessage = run.abortMessage;
|
|
386
|
+
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
313
387
|
await cursorLiveRuns.release(run);
|
|
314
|
-
|
|
388
|
+
outcome = "aborted";
|
|
389
|
+
outcomeDetails = { reason: "cancelled" };
|
|
390
|
+
return outcome;
|
|
315
391
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
392
|
+
if (run.errorMessage) {
|
|
393
|
+
partial.stopReason = "error";
|
|
394
|
+
partial.errorMessage = run.errorMessage;
|
|
395
|
+
stream.push({ type: "error", reason: "error", error: partial });
|
|
396
|
+
await cursorLiveRuns.release(run);
|
|
397
|
+
outcome = "error";
|
|
398
|
+
return outcome;
|
|
399
|
+
}
|
|
400
|
+
if (run.done) {
|
|
401
|
+
if (options.mode === "chain_user_input") {
|
|
402
|
+
await cursorLiveRuns.release(run);
|
|
403
|
+
outcome = "chain_user_input";
|
|
404
|
+
outcomeDetails = { reason: "run_done" };
|
|
405
|
+
return outcome;
|
|
406
|
+
}
|
|
407
|
+
turn.emitter.closeAll();
|
|
408
|
+
const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
|
|
409
|
+
if (finalText) {
|
|
410
|
+
await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
|
|
411
|
+
}
|
|
412
|
+
applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
|
|
413
|
+
partial.stopReason = "stop";
|
|
414
|
+
stream.push({ type: "done", reason: "stop", message: partial });
|
|
415
|
+
await cursorLiveRuns.release(run);
|
|
416
|
+
outcome = "stop";
|
|
417
|
+
outcomeDetails = { finalTextLength: finalText.length };
|
|
418
|
+
return outcome;
|
|
320
419
|
}
|
|
321
|
-
applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
|
|
322
|
-
partial.stopReason = "stop";
|
|
323
|
-
stream.push({ type: "done", reason: "stop", message: partial });
|
|
324
|
-
await cursorLiveRuns.release(run);
|
|
325
|
-
return "stop";
|
|
326
|
-
}
|
|
327
420
|
|
|
328
|
-
|
|
421
|
+
await cursorLiveRuns.waitForProgress(run, options.signal);
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (!outcome) {
|
|
425
|
+
if (error instanceof CursorLiveRunAbortError) {
|
|
426
|
+
outcome = "aborted";
|
|
427
|
+
outcomeDetails = { reason: "signal_aborted" };
|
|
428
|
+
} else {
|
|
429
|
+
outcome = "error";
|
|
430
|
+
outcomeDetails = {
|
|
431
|
+
reason: "drain_error",
|
|
432
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw error;
|
|
437
|
+
} finally {
|
|
438
|
+
debugRecorder?.recordDrainEvent("turn_end", {
|
|
439
|
+
outcome: outcome ?? "error",
|
|
440
|
+
runId: run.id,
|
|
441
|
+
pendingEventCount: run.pendingEvents.length,
|
|
442
|
+
done: run.done,
|
|
443
|
+
...outcomeDetails,
|
|
444
|
+
});
|
|
329
445
|
}
|
|
330
446
|
}
|
|
331
447
|
|
|
@@ -335,10 +451,15 @@ export async function drainExistingCursorLiveRunBeforeSend(
|
|
|
335
451
|
model: Model<Api>,
|
|
336
452
|
context: Context,
|
|
337
453
|
signal?: AbortSignal,
|
|
454
|
+
turnDebugRecorder?: CursorSdkEventDebugRecorder,
|
|
338
455
|
): Promise<LiveRunPreSendOutcome> {
|
|
456
|
+
turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
|
|
339
457
|
while (true) {
|
|
340
458
|
const run = getPendingCursorLiveRun(context) ?? getActiveCursorLiveRunForCurrentScope();
|
|
341
|
-
if (!run || run.disposed)
|
|
459
|
+
if (!run || run.disposed) {
|
|
460
|
+
turnDebugRecorder?.recordDrainEvent("pre_send_end", { outcome: "continue_send", reason: "no_pending_run" });
|
|
461
|
+
return "continue_send";
|
|
462
|
+
}
|
|
342
463
|
|
|
343
464
|
try {
|
|
344
465
|
const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
|
|
@@ -354,14 +475,28 @@ export async function drainExistingCursorLiveRunBeforeSend(
|
|
|
354
475
|
const drainOutcome = await drainCursorLiveRunTurn(stream, partial, model, context, run, consumed.toolResultInputTokens, {
|
|
355
476
|
mode: shouldChainUserInput ? "chain_user_input" : "emit",
|
|
356
477
|
signal,
|
|
478
|
+
debugRecorder: turnDebugRecorder,
|
|
479
|
+
});
|
|
480
|
+
const mapped = drainOutcome === "chain_user_input" ? "continue_send" : "stream_ended";
|
|
481
|
+
turnDebugRecorder?.recordDrainEvent("pre_send_iteration", {
|
|
482
|
+
runId: run.id,
|
|
483
|
+
drainOutcome,
|
|
484
|
+
outcome: mapped,
|
|
485
|
+
shouldChainUserInput,
|
|
357
486
|
});
|
|
358
|
-
return
|
|
487
|
+
return mapped;
|
|
359
488
|
});
|
|
360
489
|
if (outcome === "continue_send" && !run.disposed && cursorLiveRuns.getActiveForScope(run.sessionAgentScopeKey) === run) {
|
|
361
490
|
continue;
|
|
362
491
|
}
|
|
492
|
+
turnDebugRecorder?.recordDrainEvent("pre_send_end", { outcome, runId: run.id });
|
|
363
493
|
return outcome;
|
|
364
494
|
} catch (error) {
|
|
495
|
+
turnDebugRecorder?.recordDrainEvent("pre_send_end", {
|
|
496
|
+
outcome: error instanceof CursorLiveRunAbortError ? "aborted" : "error",
|
|
497
|
+
runId: run.id,
|
|
498
|
+
reason: error instanceof CursorLiveRunAbortError ? "signal_aborted" : "drain_error",
|
|
499
|
+
});
|
|
365
500
|
if (error instanceof CursorLiveRunAbortError) await cursorLiveRuns.release(run);
|
|
366
501
|
throw error;
|
|
367
502
|
}
|
|
@@ -376,4 +511,14 @@ export function resetCursorNativeReplayIdleDisposeMs(): void {
|
|
|
376
511
|
cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
377
512
|
}
|
|
378
513
|
|
|
514
|
+
export async function releaseAllPendingCursorLiveRunsForTests(): Promise<void> {
|
|
515
|
+
while (cursorLiveRuns.count() > 0) {
|
|
516
|
+
const run = cursorLiveRuns.getActiveForScope();
|
|
517
|
+
if (!run) break;
|
|
518
|
+
const before = cursorLiveRuns.count();
|
|
519
|
+
await cursorLiveRuns.release(run);
|
|
520
|
+
if (cursorLiveRuns.count() >= before) break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
379
524
|
export { hasTrailingUserMessagesAfterToolResults };
|