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