pi-cursor-sdk 0.1.19 → 0.1.21
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 +52 -0
- package/README.md +72 -11
- package/docs/cursor-dogfood-checklist.md +57 -0
- package/docs/cursor-live-smoke-checklist.md +116 -10
- package/docs/cursor-model-ux-spec.md +60 -19
- package/docs/cursor-native-tool-replay.md +21 -11
- package/docs/cursor-native-tool-visual-audit.md +104 -59
- package/docs/cursor-testing-lessons.md +10 -5
- package/docs/cursor-tool-surfaces.md +69 -0
- package/package.json +37 -11
- package/scripts/debug-provider-events.d.mts +59 -0
- package/scripts/debug-provider-events.mjs +70 -175
- package/scripts/debug-sdk-events.d.mts +90 -0
- package/scripts/debug-sdk-events.mjs +36 -98
- package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
- package/scripts/isolated-cursor-smoke.sh +264 -102
- package/scripts/lib/cursor-child-process.d.mts +10 -0
- package/scripts/lib/cursor-child-process.mjs +50 -0
- package/scripts/lib/cursor-cli-args.d.mts +63 -0
- package/scripts/lib/cursor-cli-args.mjs +129 -0
- package/scripts/lib/cursor-script-fail.d.mts +1 -0
- package/scripts/lib/cursor-script-fail.mjs +13 -0
- package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
- package/scripts/lib/cursor-smoke-env.d.mts +38 -0
- package/scripts/lib/cursor-smoke-env.mjs +81 -0
- package/scripts/lib/cursor-smoke-shell.sh +174 -0
- package/scripts/lib/cursor-visual-render.d.mts +15 -0
- package/scripts/lib/cursor-visual-render.mjs +131 -0
- package/scripts/probe-mcp-coldstart.mjs +226 -0
- package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
- package/scripts/steering-rpc-smoke.mjs +170 -65
- package/scripts/tmux-live-smoke.sh +152 -98
- package/scripts/visual-tui-smoke.mjs +659 -0
- package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
- package/shared/cursor-sdk-event-debug-env.mjs +13 -0
- package/shared/cursor-sensitive-text.d.mts +1 -0
- package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
- package/shared/cursor-setting-sources.d.mts +5 -0
- package/shared/cursor-setting-sources.mjs +22 -0
- package/src/context.ts +21 -12
- package/src/cursor-bridge-contract.ts +1 -3
- package/src/cursor-incomplete-tool-visibility.ts +72 -49
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-native-tool-display-registration.ts +63 -27
- package/src/cursor-native-tool-display-replay.ts +246 -143
- package/src/cursor-native-tool-display-state.ts +2 -0
- package/src/cursor-native-tool-display-tools.ts +149 -41
- package/src/cursor-provider-live-run-drain.ts +1 -52
- package/src/cursor-provider-run-finalizer.ts +235 -0
- package/src/cursor-provider-run-outcome.ts +149 -0
- package/src/cursor-provider-turn-api-key.ts +8 -0
- package/src/cursor-provider-turn-coordinator.ts +113 -440
- package/src/cursor-provider-turn-display-router.ts +216 -0
- package/src/cursor-provider-turn-emit.ts +59 -0
- package/src/cursor-provider-turn-finalize.ts +119 -0
- package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
- package/src/cursor-provider-turn-message-offset.ts +15 -0
- package/src/cursor-provider-turn-prepare.ts +216 -0
- package/src/cursor-provider-turn-runner.ts +138 -0
- package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
- package/src/cursor-provider-turn-send.ts +103 -0
- package/src/cursor-provider-turn-shell-output.ts +107 -0
- package/src/cursor-provider-turn-tool-ledger.ts +126 -0
- package/src/cursor-provider-turn-types.ts +87 -0
- package/src/cursor-provider.ts +16 -482
- package/src/cursor-replay-activity-builders.ts +276 -0
- package/src/cursor-replay-source-names.ts +33 -0
- package/src/cursor-replay-summary-args.ts +191 -0
- package/src/cursor-replay-tool-details.ts +464 -0
- package/src/cursor-run-final-text.ts +56 -0
- package/src/cursor-sdk-abort-error-guard.ts +4 -0
- package/src/cursor-sdk-event-debug-constants.ts +14 -5
- package/src/cursor-sdk-event-debug.ts +8 -2
- package/src/cursor-sensitive-text.ts +3 -36
- package/src/cursor-session-agent.ts +265 -88
- package/src/cursor-setting-sources.ts +7 -10
- package/src/cursor-state.ts +232 -28
- package/src/cursor-tool-lifecycle.ts +17 -42
- package/src/cursor-tool-manifest.ts +41 -0
- package/src/cursor-tool-names.ts +18 -79
- package/src/cursor-tool-presentation-registry.ts +556 -0
- package/src/cursor-tool-transcript.ts +1 -1
- package/src/cursor-tool-visibility.ts +39 -0
- package/src/cursor-transcript-tool-formatters.ts +0 -59
- package/src/cursor-transcript-tool-specs.ts +169 -232
- package/src/cursor-transcript-utils.ts +0 -44
- package/src/cursor-web-tool-activity.ts +10 -60
- package/src/cursor-web-tool-args.ts +39 -0
- package/src/index.ts +4 -10
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
2
|
+
|
|
3
|
+
export function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
4
|
+
const trimmed = apiKey?.trim();
|
|
5
|
+
if (!trimmed) return undefined;
|
|
6
|
+
if (trimmed === CURSOR_API_KEY_ENV_VAR) return process.env.CURSOR_API_KEY?.trim();
|
|
7
|
+
return trimmed;
|
|
8
|
+
}
|
|
@@ -2,103 +2,40 @@ import type { AssistantMessage, AssistantMessageEventStream } from "@earendil-wo
|
|
|
2
2
|
import type { InteractionUpdate } from "@cursor/sdk";
|
|
3
3
|
import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
4
4
|
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
5
|
-
import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
|
|
6
|
-
import { resolveNativeReplayDisposition } from "./cursor-native-replay-routing.js";
|
|
7
5
|
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
8
|
-
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
9
|
-
import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
|
|
10
|
-
import { scrubPiToolDisplay, scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
11
|
-
import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
|
|
12
6
|
import {
|
|
13
7
|
recordDiscardedIncompleteStartedToolCall,
|
|
14
8
|
type CursorSdkEventDebugRecorder,
|
|
15
|
-
DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
|
|
16
9
|
} from "./cursor-sdk-event-debug.js";
|
|
17
10
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type
|
|
11
|
+
buildIncompleteCursorToolRunOutcome,
|
|
12
|
+
resolveIncompleteCursorToolVisibility,
|
|
13
|
+
type IncompleteCursorToolRunOutcome,
|
|
21
14
|
} from "./cursor-incomplete-tool-visibility.js";
|
|
22
|
-
import { getToolName
|
|
15
|
+
import { getToolName } from "./cursor-transcript-utils.js";
|
|
16
|
+
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
17
|
+
import { buildCursorPiToolDisplay } from "./cursor-tool-transcript.js";
|
|
18
|
+
import { getField } from "./cursor-record-utils.js";
|
|
19
|
+
import { CursorTurnDisplayRouter } from "./cursor-provider-turn-display-router.js";
|
|
23
20
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from "./cursor-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface CursorShellOutputDeltas {
|
|
39
|
-
stdout: string[];
|
|
40
|
-
stderr: string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isCursorShellToolCall(toolCall: unknown): boolean {
|
|
44
|
-
const normalizedName = getToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase();
|
|
45
|
-
return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
|
|
49
|
-
if (update.type !== "shell-output-delta") return undefined;
|
|
50
|
-
const event = getField(update, "event");
|
|
51
|
-
const eventCase = getField(event, "case");
|
|
52
|
-
if (eventCase !== "stdout" && eventCase !== "stderr") return undefined;
|
|
53
|
-
const value = getField(event, "value");
|
|
54
|
-
const data = getField(value, "data");
|
|
55
|
-
if (typeof data !== "string" || data.length === 0) return undefined;
|
|
56
|
-
return { stream: eventCase, data };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: CursorShellOutputDeltas | undefined): unknown {
|
|
60
|
-
if (!deltas) return toolCall;
|
|
61
|
-
const stdout = deltas.stdout.join("");
|
|
62
|
-
const stderr = deltas.stderr.join("");
|
|
63
|
-
if (!hasUsableText(stdout) && !hasUsableText(stderr)) return toolCall;
|
|
64
|
-
|
|
65
|
-
const toolRecord = asRecord(toolCall);
|
|
66
|
-
const result = getField(toolRecord, "result");
|
|
67
|
-
const resultRecord = asRecord(result);
|
|
68
|
-
if (!toolRecord || !resultRecord || resultRecord.status !== "success") return toolCall;
|
|
69
|
-
|
|
70
|
-
const value = getField(resultRecord, "value");
|
|
71
|
-
const valueRecord = asRecord(value);
|
|
72
|
-
const completedStdout = getField(valueRecord, "stdout");
|
|
73
|
-
const completedStderr = getField(valueRecord, "stderr");
|
|
74
|
-
if (hasUsableText(typeof completedStdout === "string" ? completedStdout : undefined)) return toolCall;
|
|
75
|
-
if (hasUsableText(typeof completedStderr === "string" ? completedStderr : undefined)) return toolCall;
|
|
21
|
+
createTurnCoordinatorContentEmitter,
|
|
22
|
+
CursorToolLifecycleEmitter,
|
|
23
|
+
} from "./cursor-provider-turn-lifecycle-emitter.js";
|
|
24
|
+
import { resolveCursorToolCompletion } from "./cursor-provider-turn-sdk-normalizer.js";
|
|
25
|
+
import {
|
|
26
|
+
CursorShellOutputTracker,
|
|
27
|
+
getCursorShellOutputDelta,
|
|
28
|
+
isCursorShellToolCall,
|
|
29
|
+
} from "./cursor-provider-turn-shell-output.js";
|
|
30
|
+
import {
|
|
31
|
+
CursorToolCompletionLedger,
|
|
32
|
+
getToolFingerprint,
|
|
33
|
+
} from "./cursor-provider-turn-tool-ledger.js";
|
|
76
34
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
result: {
|
|
80
|
-
...resultRecord,
|
|
81
|
-
value: {
|
|
82
|
-
...(valueRecord ?? {}),
|
|
83
|
-
stdout,
|
|
84
|
-
stderr,
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
};
|
|
35
|
+
function getNormalizedCursorToolName(toolCall: unknown): string {
|
|
36
|
+
return classifyCursorToolVisibility(toolCall).normalizedName;
|
|
88
37
|
}
|
|
89
38
|
|
|
90
|
-
type CursorToolDisplaySource = "started" | "fallback" | "transcript";
|
|
91
|
-
|
|
92
|
-
type ToolCompletionResolution =
|
|
93
|
-
| { action: "ignore-bridge"; identity?: string }
|
|
94
|
-
| {
|
|
95
|
-
action: "handle";
|
|
96
|
-
toolCall: unknown;
|
|
97
|
-
identity?: string;
|
|
98
|
-
source?: CursorToolDisplaySource;
|
|
99
|
-
matchedStartedCallId?: string;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
39
|
export interface CursorSdkTurnCoordinatorOptions {
|
|
103
40
|
stream: AssistantMessageEventStream;
|
|
104
41
|
partial: AssistantMessage;
|
|
@@ -123,21 +60,12 @@ export class CursorSdkTurnCoordinator {
|
|
|
123
60
|
readonly nativeReplayId: string;
|
|
124
61
|
readonly textDeltas: string[];
|
|
125
62
|
|
|
126
|
-
private readonly contentEmitter: CursorPartialContentEmitter;
|
|
127
63
|
private readonly debugRecorder?: CursorSdkEventDebugRecorder;
|
|
128
|
-
private
|
|
129
|
-
private
|
|
130
|
-
private
|
|
131
|
-
private readonly
|
|
132
|
-
private readonly
|
|
133
|
-
private readonly activeShellCallIds = new Set<string>();
|
|
134
|
-
private readonly ambiguousShellOutputCallIds = new Set<string>();
|
|
135
|
-
private readonly shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
|
|
136
|
-
private readonly completedToolIdentities = new Set<string>();
|
|
137
|
-
private readonly completedStartedToolFingerprints = new Set<string>();
|
|
138
|
-
private readonly completedFallbackToolFingerprints = new Set<string>();
|
|
139
|
-
private readonly emittedLifecycleCallIds = new Set<string>();
|
|
140
|
-
private readonly lifecycleTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
64
|
+
private readonly ledger = new CursorToolCompletionLedger();
|
|
65
|
+
private readonly shellOutput = new CursorShellOutputTracker();
|
|
66
|
+
private readonly displayRouter: CursorTurnDisplayRouter;
|
|
67
|
+
private readonly lifecycleEmitter: CursorToolLifecycleEmitter;
|
|
68
|
+
private readonly contentEmitter;
|
|
141
69
|
|
|
142
70
|
constructor(options: CursorSdkTurnCoordinatorOptions) {
|
|
143
71
|
this.stream = options.stream;
|
|
@@ -150,37 +78,61 @@ export class CursorSdkTurnCoordinator {
|
|
|
150
78
|
this.nativeReplayId = options.nativeReplayId;
|
|
151
79
|
this.textDeltas = options.textDeltas;
|
|
152
80
|
this.debugRecorder = options.debugRecorder;
|
|
153
|
-
this.contentEmitter =
|
|
81
|
+
this.contentEmitter = createTurnCoordinatorContentEmitter(options.stream, options.partial);
|
|
82
|
+
this.displayRouter = new CursorTurnDisplayRouter({
|
|
83
|
+
cwd: options.cwd,
|
|
84
|
+
resolvedApiKey: options.resolvedApiKey,
|
|
85
|
+
liveRun: options.liveRun,
|
|
86
|
+
useNativeToolReplay: options.useNativeToolReplay,
|
|
87
|
+
activeToolNames: options.activeToolNames,
|
|
88
|
+
nativeReplayId: options.nativeReplayId,
|
|
89
|
+
contentEmitter: this.contentEmitter,
|
|
90
|
+
debugRecorder: options.debugRecorder,
|
|
91
|
+
});
|
|
92
|
+
this.lifecycleEmitter = new CursorToolLifecycleEmitter({
|
|
93
|
+
liveRun: options.liveRun,
|
|
94
|
+
resolvedApiKey: options.resolvedApiKey,
|
|
95
|
+
contentEmitter: this.contentEmitter,
|
|
96
|
+
debugRecorder: options.debugRecorder,
|
|
97
|
+
hasStartedToolCall: (callId) => this.ledger.hasStartedToolCall(callId),
|
|
98
|
+
isBridgeMcpToolCall: (toolCall) => options.liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall) ?? false,
|
|
99
|
+
});
|
|
154
100
|
}
|
|
155
101
|
|
|
156
102
|
get planTextCandidate(): string | undefined {
|
|
157
|
-
return this.
|
|
103
|
+
return this.displayRouter.planTextCandidate;
|
|
158
104
|
}
|
|
159
105
|
|
|
160
106
|
get replayStarted(): boolean {
|
|
161
|
-
return this.nativeToolReplayStarted;
|
|
107
|
+
return this.displayRouter.nativeToolReplayStarted;
|
|
162
108
|
}
|
|
163
109
|
|
|
164
110
|
discardIncompleteStartedToolCalls(
|
|
165
|
-
|
|
111
|
+
outcome: IncompleteCursorToolRunOutcome = buildIncompleteCursorToolRunOutcome(),
|
|
166
112
|
): void {
|
|
167
|
-
for (const [callId, toolCall] of this.
|
|
168
|
-
|
|
113
|
+
for (const [callId, toolCall] of this.ledger.startedToolCallEntries()) {
|
|
114
|
+
const toolName = getNormalizedCursorToolName(toolCall);
|
|
169
115
|
recordDiscardedIncompleteStartedToolCall(this.debugRecorder, process.env, {
|
|
170
|
-
toolName
|
|
116
|
+
toolName,
|
|
171
117
|
callId,
|
|
172
|
-
reason,
|
|
118
|
+
reason: outcome.reason,
|
|
173
119
|
});
|
|
174
|
-
|
|
120
|
+
const visibilityDecision = resolveIncompleteCursorToolVisibility(toolCall, outcome);
|
|
121
|
+
if (visibilityDecision !== "emit") {
|
|
122
|
+
this.displayRouter.recordIncompleteSkip(
|
|
123
|
+
toolName,
|
|
124
|
+
visibilityDecision === "debugOnly" && outcome.assistantTextProduced
|
|
125
|
+
? "successful-run-text-produced"
|
|
126
|
+
: visibilityDecision,
|
|
127
|
+
);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const action = this.displayRouter.routeIncompleteStartedToolCall(toolCall, outcome.reason);
|
|
131
|
+
if (action) this.displayRouter.emitDisplayAction(action);
|
|
175
132
|
}
|
|
176
|
-
this.
|
|
177
|
-
this.
|
|
178
|
-
this.
|
|
179
|
-
this.ambiguousShellOutputCallIds.clear();
|
|
180
|
-
this.shellOutputDeltasByCallId.clear();
|
|
181
|
-
this.emittedLifecycleCallIds.clear();
|
|
182
|
-
for (const timer of this.lifecycleTimers.values()) clearTimeout(timer);
|
|
183
|
-
this.lifecycleTimers.clear();
|
|
133
|
+
this.ledger.clearStartedToolCalls();
|
|
134
|
+
this.shellOutput.clear();
|
|
135
|
+
this.lifecycleEmitter.clear();
|
|
184
136
|
}
|
|
185
137
|
|
|
186
138
|
closeTraceBlock(): void {
|
|
@@ -218,28 +170,37 @@ export class CursorSdkTurnCoordinator {
|
|
|
218
170
|
return;
|
|
219
171
|
}
|
|
220
172
|
if (update.type === "partial-tool-call") {
|
|
221
|
-
this.
|
|
173
|
+
this.lifecycleEmitter.maybeSchedule(update.callId, update.toolCall);
|
|
222
174
|
return;
|
|
223
175
|
}
|
|
224
176
|
if (update.type === "tool-call-started") {
|
|
225
177
|
if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
|
|
226
|
-
if (typeof update.callId === "string") this.
|
|
178
|
+
if (typeof update.callId === "string") this.ledger.markBridgeStarted(update.callId);
|
|
227
179
|
} else {
|
|
228
|
-
this.
|
|
229
|
-
this.
|
|
230
|
-
if (isCursorShellToolCall(update.toolCall)
|
|
180
|
+
this.lifecycleEmitter.maybeSchedule(update.callId, update.toolCall);
|
|
181
|
+
this.ledger.registerStartedToolCall(update.callId, update.toolCall);
|
|
182
|
+
if (isCursorShellToolCall(update.toolCall) && typeof update.callId === "string") {
|
|
183
|
+
this.shellOutput.onShellToolStarted(update.callId);
|
|
184
|
+
}
|
|
231
185
|
}
|
|
232
186
|
return;
|
|
233
187
|
}
|
|
234
188
|
if (update.type === "tool-call-completed") {
|
|
235
|
-
const resolution =
|
|
189
|
+
const resolution = resolveCursorToolCompletion({
|
|
236
190
|
source: "delta",
|
|
237
191
|
callId: update.callId,
|
|
238
192
|
toolCall: update.toolCall,
|
|
239
|
-
startedToolCall: this.
|
|
193
|
+
startedToolCall: this.ledger.getStartedToolCall(update.callId),
|
|
194
|
+
liveRun: this.liveRun,
|
|
195
|
+
ledger: this.ledger,
|
|
196
|
+
shellOutput: this.shellOutput,
|
|
197
|
+
onClearStartedCallId: (callId) => {
|
|
198
|
+
this.lifecycleEmitter.cancel(callId);
|
|
199
|
+
this.shellOutput.onShellToolCleared(callId);
|
|
200
|
+
},
|
|
240
201
|
});
|
|
241
202
|
if (resolution.action === "ignore-bridge") {
|
|
242
|
-
this.recordIgnoreBridgeDecision(resolution.identity, getToolName(update.toolCall), "delta");
|
|
203
|
+
this.displayRouter.recordIgnoreBridgeDecision(resolution.identity, getToolName(update.toolCall), "delta");
|
|
243
204
|
return;
|
|
244
205
|
}
|
|
245
206
|
this.handleCompletedToolCall(resolution.toolCall, {
|
|
@@ -250,7 +211,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
250
211
|
}
|
|
251
212
|
if (update.type === "shell-output-delta") {
|
|
252
213
|
const delta = getCursorShellOutputDelta(update);
|
|
253
|
-
if (delta) this.appendShellOutputDelta(delta);
|
|
214
|
+
if (delta) this.shellOutput.appendShellOutputDelta(delta);
|
|
254
215
|
return;
|
|
255
216
|
}
|
|
256
217
|
if (update.type === "summary") {
|
|
@@ -272,13 +233,20 @@ export class CursorSdkTurnCoordinator {
|
|
|
272
233
|
const stepId = getField(stepEnvelope, "id") ?? getField(toolCall, "id") ?? getField(toolCall, "callId");
|
|
273
234
|
if (!toolCall) return;
|
|
274
235
|
|
|
275
|
-
const resolution =
|
|
236
|
+
const resolution = resolveCursorToolCompletion({
|
|
276
237
|
source: "step",
|
|
277
238
|
callId: stepId,
|
|
278
239
|
toolCall,
|
|
240
|
+
liveRun: this.liveRun,
|
|
241
|
+
ledger: this.ledger,
|
|
242
|
+
shellOutput: this.shellOutput,
|
|
243
|
+
onClearStartedCallId: (callId) => {
|
|
244
|
+
this.lifecycleEmitter.cancel(callId);
|
|
245
|
+
this.shellOutput.onShellToolCleared(callId);
|
|
246
|
+
},
|
|
279
247
|
});
|
|
280
248
|
if (resolution.action === "ignore-bridge") {
|
|
281
|
-
this.recordIgnoreBridgeDecision(resolution.identity, getToolName(toolCall), "step");
|
|
249
|
+
this.displayRouter.recordIgnoreBridgeDecision(resolution.identity, getToolName(toolCall), "step");
|
|
282
250
|
return;
|
|
283
251
|
}
|
|
284
252
|
this.handleCompletedToolCall(resolution.toolCall, {
|
|
@@ -286,66 +254,8 @@ export class CursorSdkTurnCoordinator {
|
|
|
286
254
|
source: resolution.source,
|
|
287
255
|
});
|
|
288
256
|
if (resolution.matchedStartedCallId && resolution.matchedStartedCallId !== stepId) {
|
|
289
|
-
this.
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private resolveToolCompletion(options: {
|
|
294
|
-
source: "delta" | "step";
|
|
295
|
-
callId: unknown;
|
|
296
|
-
toolCall: unknown;
|
|
297
|
-
startedToolCall?: unknown;
|
|
298
|
-
}): ToolCompletionResolution {
|
|
299
|
-
const bridgeStartedCallId = this.takeBridgeStartedToolCallId(options.callId);
|
|
300
|
-
if (bridgeStartedCallId) {
|
|
301
|
-
this.completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
|
|
302
|
-
return { action: "ignore-bridge", identity: `cursor-tool:${bridgeStartedCallId}` };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
let matchedStartedCallId: string | undefined;
|
|
306
|
-
let resolvedToolCall: unknown;
|
|
307
|
-
let identity: string | undefined;
|
|
308
|
-
let source: "started" | "fallback" | undefined;
|
|
309
|
-
|
|
310
|
-
if (options.source === "delta") {
|
|
311
|
-
const callId = options.callId;
|
|
312
|
-
identity = typeof callId === "string" ? `cursor-tool:${callId}` : undefined;
|
|
313
|
-
resolvedToolCall = mergeCursorToolCalls(options.startedToolCall, options.toolCall);
|
|
314
|
-
if (typeof callId === "string") {
|
|
315
|
-
this.clearStartedToolCall(callId);
|
|
316
|
-
}
|
|
317
|
-
resolvedToolCall = mergeShellOutputDeltasIntoCursorToolCall(
|
|
318
|
-
resolvedToolCall,
|
|
319
|
-
typeof callId === "string" ? this.takeShellOutputDeltas(callId) : undefined,
|
|
320
|
-
);
|
|
321
|
-
source = identity ? "started" : "fallback";
|
|
322
|
-
} else {
|
|
323
|
-
matchedStartedCallId = this.removeStartedToolCallForStep(options.toolCall, options.callId);
|
|
324
|
-
resolvedToolCall = mergeShellOutputDeltasIntoCursorToolCall(
|
|
325
|
-
options.toolCall,
|
|
326
|
-
matchedStartedCallId ? this.takeShellOutputDeltas(matchedStartedCallId) : undefined,
|
|
327
|
-
);
|
|
328
|
-
const identityId = typeof options.callId === "string" ? options.callId : matchedStartedCallId;
|
|
329
|
-
identity = identityId ? `cursor-tool:${identityId}` : undefined;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(resolvedToolCall)) {
|
|
333
|
-
const bridgeIdentity = options.source === "step" && matchedStartedCallId
|
|
334
|
-
? `cursor-tool:${matchedStartedCallId}`
|
|
335
|
-
: identity;
|
|
336
|
-
if (bridgeIdentity) this.completedToolIdentities.add(bridgeIdentity);
|
|
337
|
-
return { action: "ignore-bridge", identity: bridgeIdentity };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (options.source === "delta") {
|
|
341
|
-
return { action: "handle", toolCall: resolvedToolCall, identity, source };
|
|
257
|
+
this.ledger.recordCompletedIdentity(`cursor-tool:${resolution.matchedStartedCallId}`);
|
|
342
258
|
}
|
|
343
|
-
return {
|
|
344
|
-
action: "handle",
|
|
345
|
-
toolCall: resolvedToolCall,
|
|
346
|
-
identity,
|
|
347
|
-
matchedStartedCallId,
|
|
348
|
-
};
|
|
349
259
|
}
|
|
350
260
|
|
|
351
261
|
handleTranscriptCompletedToolCalls(toolCalls: readonly { identity: string; toolCall: unknown }[]): void {
|
|
@@ -356,271 +266,34 @@ export class CursorSdkTurnCoordinator {
|
|
|
356
266
|
|
|
357
267
|
private handleCompletedToolCall(
|
|
358
268
|
toolCall: unknown,
|
|
359
|
-
options: { identity?: string; source?:
|
|
269
|
+
options: { identity?: string; source?: "started" | "fallback" | "transcript" } = {},
|
|
360
270
|
): void {
|
|
361
|
-
const planText = getCursorCreatePlanText(toolCall);
|
|
362
|
-
if (planText) this.cursorPlanTextCandidate = scrubSensitiveText(planText, this.resolvedApiKey);
|
|
363
|
-
|
|
364
|
-
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd: this.cwd }), this.resolvedApiKey);
|
|
365
271
|
const display = buildCursorPiToolDisplay(toolCall, { cwd: this.cwd });
|
|
366
|
-
const
|
|
367
|
-
const fingerprint = this.getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
|
|
368
|
-
if (options.identity && this.completedToolIdentities.has(options.identity)) {
|
|
369
|
-
this.recordDisplayDecision({
|
|
370
|
-
action: "skip-duplicate",
|
|
371
|
-
toolName,
|
|
372
|
-
identity: options.identity,
|
|
373
|
-
source: options.source,
|
|
374
|
-
reason: "identity-already-completed",
|
|
375
|
-
});
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (options.source === "started") {
|
|
379
|
-
if (this.completedFallbackToolFingerprints.has(fingerprint)) {
|
|
380
|
-
this.recordDisplayDecision({
|
|
381
|
-
action: "skip-duplicate",
|
|
382
|
-
toolName,
|
|
383
|
-
identity: options.identity,
|
|
384
|
-
source: options.source,
|
|
385
|
-
reason: "fallback-fingerprint-already-completed",
|
|
386
|
-
});
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
} else if (this.completedStartedToolFingerprints.has(fingerprint) || this.completedFallbackToolFingerprints.has(fingerprint)) {
|
|
390
|
-
this.recordDisplayDecision({
|
|
391
|
-
action: "skip-duplicate",
|
|
392
|
-
toolName,
|
|
393
|
-
identity: options.identity,
|
|
394
|
-
source: options.source,
|
|
395
|
-
reason: "fingerprint-already-completed",
|
|
396
|
-
});
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
if (options.identity) this.completedToolIdentities.add(options.identity);
|
|
400
|
-
if (options.source === "started") {
|
|
401
|
-
this.completedStartedToolFingerprints.add(fingerprint);
|
|
402
|
-
} else {
|
|
403
|
-
this.completedFallbackToolFingerprints.add(fingerprint);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const disposition = resolveNativeReplayDisposition({
|
|
272
|
+
const fingerprint = getToolFingerprint({
|
|
407
273
|
toolName: display.toolName,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
hasLiveRun: this.liveRun !== undefined,
|
|
274
|
+
args: display.args,
|
|
275
|
+
result: display.result,
|
|
411
276
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
disposition,
|
|
420
|
-
toolName,
|
|
277
|
+
const duplicateReason = this.ledger.shouldSkipDuplicateCompletion({
|
|
278
|
+
identity: options.identity,
|
|
279
|
+
source: options.source,
|
|
280
|
+
fingerprint,
|
|
281
|
+
});
|
|
282
|
+
if (duplicateReason) {
|
|
283
|
+
this.displayRouter.recordDuplicateSkip(display.toolName, {
|
|
421
284
|
identity: options.identity,
|
|
422
285
|
source: options.source,
|
|
423
|
-
|
|
424
|
-
replayToolId: id,
|
|
425
|
-
});
|
|
426
|
-
cursorLiveRuns.queueEvent(this.liveRun, {
|
|
427
|
-
type: "tool",
|
|
428
|
-
tool: { ...scrubbedDisplay, id },
|
|
286
|
+
reason: duplicateReason,
|
|
429
287
|
});
|
|
430
288
|
return;
|
|
431
289
|
}
|
|
432
|
-
|
|
433
|
-
const traceText =
|
|
434
|
-
disposition === "inactive_trace"
|
|
435
|
-
? formatInactiveCursorReplayTrace(scrubPiToolDisplay(display, this.resolvedApiKey))
|
|
436
|
-
: transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`;
|
|
437
|
-
this.recordDisplayDecision({
|
|
438
|
-
action: "emit_trace",
|
|
439
|
-
disposition,
|
|
440
|
-
toolName,
|
|
290
|
+
this.ledger.recordCompletedTool({
|
|
441
291
|
identity: options.identity,
|
|
442
292
|
source: options.source,
|
|
443
|
-
|
|
444
|
-
traceText,
|
|
445
|
-
});
|
|
446
|
-
this.emitCursorToolTrace(traceText);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private emitIncompleteStartedToolCall(toolCall: unknown, reason: IncompleteCursorToolDiscardReason): void {
|
|
450
|
-
const display = scrubPiToolDisplay(
|
|
451
|
-
buildIncompleteCursorToolDisplay(toolCall, reason, { apiKey: this.resolvedApiKey }),
|
|
452
|
-
this.resolvedApiKey,
|
|
453
|
-
);
|
|
454
|
-
const toolName = display.toolName;
|
|
455
|
-
const disposition = resolveNativeReplayDisposition({
|
|
456
|
-
toolName,
|
|
457
|
-
useNativeToolReplay: this.useNativeToolReplay,
|
|
458
|
-
activeToolNames: this.activeToolNames,
|
|
459
|
-
hasLiveRun: this.liveRun !== undefined,
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// Aborted live runs emit trace visibility only; do not synthesize a toolUse replay turn.
|
|
463
|
-
if (disposition === "queue_replay" && this.liveRun && reason !== "abort") {
|
|
464
|
-
this.nativeToolReplayStarted = true;
|
|
465
|
-
const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
|
|
466
|
-
this.recordDisplayDecision({
|
|
467
|
-
action: "queue_replay",
|
|
468
|
-
disposition,
|
|
469
|
-
toolName,
|
|
470
|
-
source: "started",
|
|
471
|
-
reason: "incomplete-started-tool-call",
|
|
472
|
-
replayToolId: id,
|
|
473
|
-
});
|
|
474
|
-
cursorLiveRuns.queueEvent(this.liveRun, {
|
|
475
|
-
type: "tool",
|
|
476
|
-
tool: { ...display, id },
|
|
477
|
-
});
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const traceText =
|
|
482
|
-
disposition === "inactive_trace"
|
|
483
|
-
? formatInactiveCursorReplayTrace(display)
|
|
484
|
-
: formatIncompleteCursorToolTrace(display);
|
|
485
|
-
this.recordDisplayDecision({
|
|
486
|
-
action: "emit_trace",
|
|
487
|
-
disposition,
|
|
488
|
-
toolName,
|
|
489
|
-
source: "started",
|
|
490
|
-
reason: "incomplete-started-tool-call",
|
|
491
|
-
traceText,
|
|
293
|
+
fingerprint,
|
|
492
294
|
});
|
|
493
|
-
this.emitCursorToolTrace(traceText);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
private recordIgnoreBridgeDecision(
|
|
497
|
-
identity: string | undefined,
|
|
498
|
-
toolName: string,
|
|
499
|
-
source: "delta" | "step",
|
|
500
|
-
): void {
|
|
501
|
-
this.debugRecorder?.recordDisplayDecision({
|
|
502
|
-
action: "ignore-bridge",
|
|
503
|
-
toolName,
|
|
504
|
-
identity,
|
|
505
|
-
source,
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
private recordDisplayDecision(decision: Parameters<CursorSdkEventDebugRecorder["recordDisplayDecision"]>[0]): void {
|
|
510
|
-
this.debugRecorder?.recordDisplayDecision(decision);
|
|
511
|
-
}
|
|
512
295
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (this.liveRun) {
|
|
516
|
-
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: traceText });
|
|
517
|
-
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-completed" });
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
this.contentEmitter.appendThinkingBlock(traceText);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
private maybeScheduleCursorToolLifecycle(callId: unknown, toolCall: unknown): void {
|
|
524
|
-
if (typeof callId !== "string" || this.emittedLifecycleCallIds.has(callId)) return;
|
|
525
|
-
if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) return;
|
|
526
|
-
if (!isCursorToolLifecycleEligible(toolCall)) return;
|
|
527
|
-
|
|
528
|
-
this.cancelCursorToolLifecycleTimer(callId);
|
|
529
|
-
const timer = setTimeout(() => {
|
|
530
|
-
this.lifecycleTimers.delete(callId);
|
|
531
|
-
if (!this.startedToolCalls.has(callId)) return;
|
|
532
|
-
if (this.emittedLifecycleCallIds.has(callId)) return;
|
|
533
|
-
this.emitCursorToolLifecycle(callId, toolCall);
|
|
534
|
-
}, CURSOR_TOOL_LIFECYCLE_DEFER_MS);
|
|
535
|
-
timer.unref?.();
|
|
536
|
-
this.lifecycleTimers.set(callId, timer);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
private cancelCursorToolLifecycleTimer(callId: string): void {
|
|
540
|
-
const timer = this.lifecycleTimers.get(callId);
|
|
541
|
-
if (!timer) return;
|
|
542
|
-
clearTimeout(timer);
|
|
543
|
-
this.lifecycleTimers.delete(callId);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private emitCursorToolLifecycle(callId: string, toolCall: unknown): void {
|
|
547
|
-
const progressText = formatCursorToolLifecycleProgressText(toolCall, this.resolvedApiKey);
|
|
548
|
-
if (!progressText) return;
|
|
549
|
-
this.emittedLifecycleCallIds.add(callId);
|
|
550
|
-
this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle", {
|
|
551
|
-
callId,
|
|
552
|
-
toolName: normalizeToolName(getToolName(toolCall)),
|
|
553
|
-
progressText,
|
|
554
|
-
liveRun: this.liveRun !== undefined,
|
|
555
|
-
});
|
|
556
|
-
if (this.liveRun) {
|
|
557
|
-
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: progressText });
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
this.contentEmitter.appendThinkingDelta(progressText);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
private getToolFingerprint(value: unknown): string {
|
|
564
|
-
try {
|
|
565
|
-
return JSON.stringify(value);
|
|
566
|
-
} catch {
|
|
567
|
-
return String(value);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
private getStartedToolCallFingerprint(toolCall: unknown): string {
|
|
572
|
-
return this.getToolFingerprint({ toolName: getToolName(toolCall), args: getField(toolCall, "args") });
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private clearStartedToolCall(callId: string): void {
|
|
576
|
-
this.cancelCursorToolLifecycleTimer(callId);
|
|
577
|
-
this.startedToolCalls.delete(callId);
|
|
578
|
-
this.bridgeStartedToolCallIds.delete(callId);
|
|
579
|
-
this.activeShellCallIds.delete(callId);
|
|
580
|
-
this.ambiguousShellOutputCallIds.delete(callId);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
private takeBridgeStartedToolCallId(callId: unknown): string | undefined {
|
|
584
|
-
if (typeof callId !== "string" || !this.bridgeStartedToolCallIds.has(callId)) return undefined;
|
|
585
|
-
this.bridgeStartedToolCallIds.delete(callId);
|
|
586
|
-
return callId;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
private takeShellOutputDeltas(callId: string): CursorShellOutputDeltas | undefined {
|
|
590
|
-
const deltas = this.shellOutputDeltasByCallId.get(callId);
|
|
591
|
-
this.shellOutputDeltasByCallId.delete(callId);
|
|
592
|
-
return deltas;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private appendShellOutputDelta(delta: CursorShellOutputDelta): void {
|
|
596
|
-
if (this.activeShellCallIds.size !== 1) {
|
|
597
|
-
for (const activeCallId of this.activeShellCallIds) {
|
|
598
|
-
this.ambiguousShellOutputCallIds.add(activeCallId);
|
|
599
|
-
this.shellOutputDeltasByCallId.delete(activeCallId);
|
|
600
|
-
}
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
const [callId] = this.activeShellCallIds;
|
|
604
|
-
if (!callId || this.ambiguousShellOutputCallIds.has(callId)) return;
|
|
605
|
-
let deltas = this.shellOutputDeltasByCallId.get(callId);
|
|
606
|
-
if (!deltas) {
|
|
607
|
-
deltas = { stdout: [], stderr: [] };
|
|
608
|
-
this.shellOutputDeltasByCallId.set(callId, deltas);
|
|
609
|
-
}
|
|
610
|
-
deltas[delta.stream].push(delta.data);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
private removeStartedToolCallForStep(toolCall: unknown, stepId: unknown): string | undefined {
|
|
614
|
-
if (typeof stepId === "string" && this.startedToolCalls.has(stepId)) {
|
|
615
|
-
this.clearStartedToolCall(stepId);
|
|
616
|
-
return stepId;
|
|
617
|
-
}
|
|
618
|
-
const fingerprint = this.getStartedToolCallFingerprint(toolCall);
|
|
619
|
-
for (const [callId, startedToolCall] of this.startedToolCalls) {
|
|
620
|
-
if (this.getStartedToolCallFingerprint(startedToolCall) !== fingerprint) continue;
|
|
621
|
-
this.clearStartedToolCall(callId);
|
|
622
|
-
return callId;
|
|
623
|
-
}
|
|
624
|
-
return undefined;
|
|
296
|
+
const action = this.displayRouter.routeCompletedToolCall(toolCall, options);
|
|
297
|
+
if (action) this.displayRouter.emitDisplayAction(action);
|
|
625
298
|
}
|
|
626
299
|
}
|