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,216 @@
|
|
|
1
|
+
import type { SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Agent } from "@cursor/sdk";
|
|
3
|
+
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
4
|
+
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
|
|
5
|
+
import {
|
|
6
|
+
acquireSessionCursorAgent,
|
|
7
|
+
buildCursorSessionSendPrompt,
|
|
8
|
+
planCursorSessionSend,
|
|
9
|
+
resetSessionCursorAgent,
|
|
10
|
+
} from "./cursor-session-agent.js";
|
|
11
|
+
import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
|
|
12
|
+
import { estimateCursorPromptInputTokens, getCursorPromptOptions } from "./cursor-usage-accounting.js";
|
|
13
|
+
import { getActiveContextToolNames } from "./cursor-context-tools.js";
|
|
14
|
+
import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
15
|
+
import { abandonSessionCursorAgent, cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
16
|
+
import { createCursorNativeReplayId } from "./cursor-provider-live-run-drain.js";
|
|
17
|
+
import { getEffectiveCursorAgentMode, getEffectiveFastForModelId } from "./cursor-state.js";
|
|
18
|
+
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
19
|
+
import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
|
|
20
|
+
import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-snapshot.js";
|
|
21
|
+
import {
|
|
22
|
+
buildCursorToolManifestText,
|
|
23
|
+
resolveCursorToolManifestEnabled,
|
|
24
|
+
} from "./cursor-tool-manifest.js";
|
|
25
|
+
import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display.js";
|
|
26
|
+
import { MISSING_CURSOR_API_KEY_MESSAGE } from "./cursor-provider-errors.js";
|
|
27
|
+
import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
28
|
+
import { resolveCursorApiKey } from "./cursor-provider-turn-api-key.js";
|
|
29
|
+
import type {
|
|
30
|
+
CursorProviderTurnPrepareResult,
|
|
31
|
+
CursorProviderTurnRunnerParams,
|
|
32
|
+
} from "./cursor-provider-turn-types.js";
|
|
33
|
+
import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
34
|
+
|
|
35
|
+
export interface PrepareCursorProviderTurnParams {
|
|
36
|
+
params: CursorProviderTurnRunnerParams;
|
|
37
|
+
cwd: string;
|
|
38
|
+
resolvedApiKey: string;
|
|
39
|
+
sdkEventDebug: CursorSdkEventDebugSink | undefined;
|
|
40
|
+
throwIfAborted: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function prepareCursorProviderTurn(
|
|
44
|
+
prepareParams: PrepareCursorProviderTurnParams,
|
|
45
|
+
): Promise<CursorProviderTurnPrepareResult> {
|
|
46
|
+
const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted } = prepareParams;
|
|
47
|
+
const { model, context, options } = params;
|
|
48
|
+
|
|
49
|
+
let restoreCursorSdkOutputFilter: (() => void) | undefined;
|
|
50
|
+
let sessionAgentScopeKey: string | undefined;
|
|
51
|
+
let liveRun: CursorLiveRun | undefined;
|
|
52
|
+
let completed = false;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const fastEnabled = getEffectiveFastForModelId(model.id);
|
|
56
|
+
const agentMode = getEffectiveCursorAgentMode();
|
|
57
|
+
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
58
|
+
const settingSources = getEffectiveCursorSettingSources();
|
|
59
|
+
|
|
60
|
+
installCursorMcpToolTimeoutOverride();
|
|
61
|
+
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
62
|
+
const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
|
|
63
|
+
let liveRunForBridgeQueue: CursorLiveRun | undefined;
|
|
64
|
+
|
|
65
|
+
const sessionAgentAcquireParams = {
|
|
66
|
+
apiKey: resolvedApiKey,
|
|
67
|
+
agentMode,
|
|
68
|
+
cwd,
|
|
69
|
+
modelSelection: selection,
|
|
70
|
+
settingSources,
|
|
71
|
+
debugRecorder: sdkEventDebug,
|
|
72
|
+
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
73
|
+
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
74
|
+
cursorLiveRuns.queueEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
75
|
+
} else {
|
|
76
|
+
queuedBridgeRequestsBeforeLiveRun.push(request);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
createAgent: (createOptions: Parameters<typeof Agent.create>[0]) =>
|
|
80
|
+
suppressCursorSdkOutput(() => Agent.create(createOptions)),
|
|
81
|
+
};
|
|
82
|
+
let sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
83
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
84
|
+
throwIfAborted();
|
|
85
|
+
|
|
86
|
+
const buildPromptOptions = (plan: ReturnType<typeof planCursorSessionSend>) => {
|
|
87
|
+
const promptOptions = getCursorPromptOptions(model);
|
|
88
|
+
if (plan.mode !== "bootstrap" || !resolveCursorToolManifestEnabled()) {
|
|
89
|
+
return promptOptions;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...promptOptions,
|
|
93
|
+
toolManifest: buildCursorToolManifestText({
|
|
94
|
+
bridgeSnapshot: sessionAgentLease.bridgeRun?.snapshot,
|
|
95
|
+
piBridgeEnabled: resolveCursorPiToolBridgeEnabled(),
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
let sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
|
|
100
|
+
let promptOptions = buildPromptOptions(sendPlan);
|
|
101
|
+
let prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
|
|
102
|
+
if (sendPlan.resetAgent) {
|
|
103
|
+
await resetSessionCursorAgent(sessionAgentScopeKey);
|
|
104
|
+
sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
105
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
106
|
+
sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
|
|
107
|
+
promptOptions = buildPromptOptions(sendPlan);
|
|
108
|
+
prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
|
|
109
|
+
}
|
|
110
|
+
const bootstrap = sendPlan.mode === "bootstrap";
|
|
111
|
+
const agent = sessionAgentLease.agent;
|
|
112
|
+
const bridgeRun = sessionAgentLease.bridgeRun;
|
|
113
|
+
const sendPayload = {
|
|
114
|
+
text: prompt.text,
|
|
115
|
+
images: prompt.images.length > 0 ? prompt.images : undefined,
|
|
116
|
+
};
|
|
117
|
+
const sessionBridgeRun = bridgeRun;
|
|
118
|
+
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
119
|
+
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
120
|
+
const activeToolNames = getActiveContextToolNames(context);
|
|
121
|
+
sdkEventDebug?.recordProviderMeta({
|
|
122
|
+
model: {
|
|
123
|
+
id: model.id,
|
|
124
|
+
provider: model.provider,
|
|
125
|
+
api: model.api,
|
|
126
|
+
reasoning: options?.reasoning ?? "off",
|
|
127
|
+
fastEnabled,
|
|
128
|
+
selection,
|
|
129
|
+
},
|
|
130
|
+
settingSources: settingSources ?? null,
|
|
131
|
+
sendState: sessionAgentLease.sendState,
|
|
132
|
+
sendPlan,
|
|
133
|
+
promptOptions,
|
|
134
|
+
toolManifestEnabled: resolveCursorToolManifestEnabled(),
|
|
135
|
+
agentMode,
|
|
136
|
+
activeToolNames: activeToolNames ? [...activeToolNames] : [],
|
|
137
|
+
sessionAgentScopeKey,
|
|
138
|
+
bridgeRunId: bridgeRun?.id,
|
|
139
|
+
});
|
|
140
|
+
const nativeReplayId = createCursorNativeReplayId();
|
|
141
|
+
const textDeltas: string[] = [];
|
|
142
|
+
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
143
|
+
liveRun = useLiveRun
|
|
144
|
+
? cursorLiveRuns.start({
|
|
145
|
+
id: useNativeToolReplay ? nativeReplayId : bridgeRun?.id ?? nativeReplayId,
|
|
146
|
+
agent,
|
|
147
|
+
bridgeRun,
|
|
148
|
+
sessionBridgeRun,
|
|
149
|
+
sessionAgentScopeKey,
|
|
150
|
+
promptInputTokens,
|
|
151
|
+
textDeltas,
|
|
152
|
+
debugRecorder: sdkEventDebug,
|
|
153
|
+
})
|
|
154
|
+
: undefined;
|
|
155
|
+
if (liveRun) {
|
|
156
|
+
liveRunForBridgeQueue = liveRun;
|
|
157
|
+
for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
|
|
158
|
+
cursorLiveRuns.queueEvent(liveRun, { type: "bridge-tool", request });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const turnCoordinator = new CursorSdkTurnCoordinator({
|
|
162
|
+
stream: params.stream,
|
|
163
|
+
partial: params.partial,
|
|
164
|
+
cwd,
|
|
165
|
+
resolvedApiKey,
|
|
166
|
+
liveRun,
|
|
167
|
+
useNativeToolReplay,
|
|
168
|
+
activeToolNames,
|
|
169
|
+
nativeReplayId,
|
|
170
|
+
textDeltas,
|
|
171
|
+
debugRecorder: sdkEventDebug,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
completed = true;
|
|
175
|
+
return {
|
|
176
|
+
agent,
|
|
177
|
+
cwd,
|
|
178
|
+
payload: sendPayload,
|
|
179
|
+
meta: {
|
|
180
|
+
sendPlan,
|
|
181
|
+
prompt,
|
|
182
|
+
bootstrap,
|
|
183
|
+
promptInputTokens,
|
|
184
|
+
useNativeToolReplay,
|
|
185
|
+
bridgeEnabled: bridgeRun !== undefined,
|
|
186
|
+
nativeReplayId,
|
|
187
|
+
agentMode,
|
|
188
|
+
},
|
|
189
|
+
contextWindowAgentId: agent.agentId,
|
|
190
|
+
textDeltas,
|
|
191
|
+
sessionAgentScopeKey,
|
|
192
|
+
sessionAgentLease,
|
|
193
|
+
restoreCursorSdkOutputFilter,
|
|
194
|
+
runtime: liveRun
|
|
195
|
+
? { kind: "live", liveRun, turnCoordinator }
|
|
196
|
+
: { kind: "direct", turnCoordinator },
|
|
197
|
+
};
|
|
198
|
+
} finally {
|
|
199
|
+
if (!completed) {
|
|
200
|
+
if (liveRun && !liveRun.disposed) {
|
|
201
|
+
await cursorLiveRuns
|
|
202
|
+
.release(liveRun)
|
|
203
|
+
.catch(() => abandonSessionCursorAgent(sessionAgentScopeKey).catch(() => {}));
|
|
204
|
+
} else {
|
|
205
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey).catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
restoreCursorSdkOutputFilter?.();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function requireCursorApiKey(options: SimpleStreamOptions | undefined): string {
|
|
213
|
+
const apiKey = resolveCursorApiKey(options?.apiKey);
|
|
214
|
+
if (!apiKey) throw new Error(MISSING_CURSOR_API_KEY_MESSAGE);
|
|
215
|
+
return apiKey;
|
|
216
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
|
|
2
|
+
import { drainExistingCursorLiveRunBeforeSend } from "./cursor-provider-live-run-drain.js";
|
|
3
|
+
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
4
|
+
import { installCursorSdkAbortErrorSuppression } from "./cursor-sdk-abort-error-guard.js";
|
|
5
|
+
import { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
6
|
+
import { awaitFinalizeCursorRunOutcome } from "./cursor-provider-turn-finalize.js";
|
|
7
|
+
import {
|
|
8
|
+
discardIncompleteToolsFromPrepared,
|
|
9
|
+
emitCursorLiveTurn,
|
|
10
|
+
} from "./cursor-provider-turn-emit.js";
|
|
11
|
+
import { CursorRunFinalizer, type CursorLiveRunCompletion } from "./cursor-provider-run-finalizer.js";
|
|
12
|
+
import { prepareCursorProviderTurn, requireCursorApiKey } from "./cursor-provider-turn-prepare.js";
|
|
13
|
+
import { sendCursorProviderTurn } from "./cursor-provider-turn-send.js";
|
|
14
|
+
import type {
|
|
15
|
+
CursorProviderTurnPrepareResult,
|
|
16
|
+
CursorProviderTurnRunnerParams,
|
|
17
|
+
CursorProviderTurnSendResult,
|
|
18
|
+
} from "./cursor-provider-turn-types.js";
|
|
19
|
+
|
|
20
|
+
export { resolveCursorApiKey } from "./cursor-provider-turn-api-key.js";
|
|
21
|
+
export type { CursorProviderTurnRunnerParams } from "./cursor-provider-turn-types.js";
|
|
22
|
+
|
|
23
|
+
export class CursorProviderTurnRunner {
|
|
24
|
+
private sdkEventDebug: CursorSdkEventDebugSink | undefined;
|
|
25
|
+
private resolvedApiKey: string | undefined;
|
|
26
|
+
|
|
27
|
+
constructor(private readonly params: CursorProviderTurnRunnerParams) {}
|
|
28
|
+
|
|
29
|
+
private get options() {
|
|
30
|
+
return this.params.options;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private throwIfAborted(): void {
|
|
34
|
+
if (this.options?.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private discardIncompleteTools(
|
|
38
|
+
prepared: CursorProviderTurnPrepareResult | undefined,
|
|
39
|
+
outcome: import("./cursor-incomplete-tool-visibility.js").IncompleteCursorToolRunOutcomeInput,
|
|
40
|
+
): void {
|
|
41
|
+
discardIncompleteToolsFromPrepared(prepared, outcome);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async run(sdkAbortErrorSuppression: ReturnType<typeof installCursorSdkAbortErrorSuppression>): Promise<void> {
|
|
45
|
+
const { stream, partial, model, context, options, sdkEventDebugRef } = this.params;
|
|
46
|
+
let prepared: CursorProviderTurnPrepareResult | undefined;
|
|
47
|
+
let sendResult: CursorProviderTurnSendResult | undefined;
|
|
48
|
+
let liveCompletion: CursorLiveRunCompletion | undefined;
|
|
49
|
+
const runFinalizer = new CursorRunFinalizer({
|
|
50
|
+
runnerParams: this.params,
|
|
51
|
+
sdkEventDebug: () => this.sdkEventDebug,
|
|
52
|
+
sdkAbortErrorSuppression,
|
|
53
|
+
resolvedApiKey: () => this.resolvedApiKey,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
stream.push({ type: "start", partial });
|
|
58
|
+
this.throwIfAborted();
|
|
59
|
+
const cwd = getCursorSessionCwd();
|
|
60
|
+
this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
|
|
61
|
+
cwd,
|
|
62
|
+
modelId: model.id,
|
|
63
|
+
provider: model.provider,
|
|
64
|
+
});
|
|
65
|
+
sdkEventDebugRef.current = this.sdkEventDebug;
|
|
66
|
+
this.sdkEventDebug?.recordContextSnapshot(context);
|
|
67
|
+
if (
|
|
68
|
+
(await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug)) ===
|
|
69
|
+
"stream_ended"
|
|
70
|
+
) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.resolvedApiKey = requireCursorApiKey(options);
|
|
75
|
+
prepared = await prepareCursorProviderTurn({
|
|
76
|
+
params: this.params,
|
|
77
|
+
cwd,
|
|
78
|
+
resolvedApiKey: this.resolvedApiKey,
|
|
79
|
+
sdkEventDebug: this.sdkEventDebug,
|
|
80
|
+
throwIfAborted: () => this.throwIfAborted(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
sendResult = await sendCursorProviderTurn({
|
|
84
|
+
params: this.params,
|
|
85
|
+
prepared,
|
|
86
|
+
sdkEventDebug: this.sdkEventDebug,
|
|
87
|
+
sdkAbortErrorSuppression,
|
|
88
|
+
throwIfAborted: () => this.throwIfAborted(),
|
|
89
|
+
});
|
|
90
|
+
const { send } = sendResult;
|
|
91
|
+
|
|
92
|
+
if (prepared.runtime.kind === "live") {
|
|
93
|
+
liveCompletion = runFinalizer.startLiveRunCompletion({
|
|
94
|
+
send,
|
|
95
|
+
prepared,
|
|
96
|
+
modelId: model.id,
|
|
97
|
+
discardIncompleteTools: (outcome) => this.discardIncompleteTools(prepared, outcome),
|
|
98
|
+
});
|
|
99
|
+
await emitCursorLiveTurn({
|
|
100
|
+
params: this.params,
|
|
101
|
+
prepared,
|
|
102
|
+
sdkEventDebug: this.sdkEventDebug,
|
|
103
|
+
discardIncompleteTools: (outcome) => this.discardIncompleteTools(prepared, outcome),
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const outcome = await awaitFinalizeCursorRunOutcome({
|
|
109
|
+
run: send.run,
|
|
110
|
+
prepared,
|
|
111
|
+
cursorAgentMessageOffset: send.cursorAgentMessageOffset,
|
|
112
|
+
modelId: model.id,
|
|
113
|
+
signal: options?.signal,
|
|
114
|
+
runResultFallback: send.run.result,
|
|
115
|
+
resolvedApiKey: this.resolvedApiKey,
|
|
116
|
+
optionsApiKey: options?.apiKey,
|
|
117
|
+
sdkEventDebug: this.sdkEventDebug,
|
|
118
|
+
contextWindowAgentId: prepared.contextWindowAgentId,
|
|
119
|
+
});
|
|
120
|
+
await runFinalizer.applyTerminalEvent({ kind: "direct", prepared, outcome });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
await runFinalizer.applyTerminalEvent({ kind: "error", prepared, error });
|
|
123
|
+
} finally {
|
|
124
|
+
await runFinalizer.cleanup(prepared, sendResult, liveCompletion);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async handleOuterCatch(error: unknown): Promise<void> {
|
|
129
|
+
const runFinalizer = new CursorRunFinalizer({
|
|
130
|
+
runnerParams: this.params,
|
|
131
|
+
sdkEventDebug: () => this.sdkEventDebug,
|
|
132
|
+
sdkAbortErrorSuppression: installCursorSdkAbortErrorSuppression(),
|
|
133
|
+
resolvedApiKey: () => this.resolvedApiKey,
|
|
134
|
+
});
|
|
135
|
+
await runFinalizer.applyTerminalEvent({ kind: "error", prepared: undefined, error });
|
|
136
|
+
await runFinalizer.cleanup(undefined, undefined, undefined);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mergeCursorToolCalls } from "./cursor-tool-transcript.js";
|
|
2
|
+
import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
3
|
+
import {
|
|
4
|
+
mergeShellOutputDeltasIntoCursorToolCall,
|
|
5
|
+
type CursorShellOutputTracker,
|
|
6
|
+
} from "./cursor-provider-turn-shell-output.js";
|
|
7
|
+
import type { CursorToolCompletionLedger } from "./cursor-provider-turn-tool-ledger.js";
|
|
8
|
+
|
|
9
|
+
export type CursorToolCompletionSource = "delta" | "step";
|
|
10
|
+
|
|
11
|
+
export type ToolCompletionResolution =
|
|
12
|
+
| { action: "ignore-bridge"; identity?: string }
|
|
13
|
+
| {
|
|
14
|
+
action: "handle";
|
|
15
|
+
toolCall: unknown;
|
|
16
|
+
identity?: string;
|
|
17
|
+
source?: "started" | "fallback";
|
|
18
|
+
matchedStartedCallId?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface ResolveCursorToolCompletionOptions {
|
|
22
|
+
source: CursorToolCompletionSource;
|
|
23
|
+
callId: unknown;
|
|
24
|
+
toolCall: unknown;
|
|
25
|
+
startedToolCall?: unknown;
|
|
26
|
+
liveRun?: CursorLiveRun;
|
|
27
|
+
ledger: CursorToolCompletionLedger;
|
|
28
|
+
shellOutput: CursorShellOutputTracker;
|
|
29
|
+
onClearStartedCallId?: (callId: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveCursorToolCompletion(options: ResolveCursorToolCompletionOptions): ToolCompletionResolution {
|
|
33
|
+
const bridgeStartedCallId = options.ledger.takeBridgeStartedCallId(
|
|
34
|
+
typeof options.callId === "string" ? options.callId : "",
|
|
35
|
+
);
|
|
36
|
+
if (bridgeStartedCallId) {
|
|
37
|
+
options.ledger.recordCompletedIdentity(`cursor-tool:${bridgeStartedCallId}`);
|
|
38
|
+
return { action: "ignore-bridge", identity: `cursor-tool:${bridgeStartedCallId}` };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let matchedStartedCallId: string | undefined;
|
|
42
|
+
let resolvedToolCall: unknown;
|
|
43
|
+
let identity: string | undefined;
|
|
44
|
+
let source: "started" | "fallback" | undefined;
|
|
45
|
+
|
|
46
|
+
if (options.source === "delta") {
|
|
47
|
+
const callId = options.callId;
|
|
48
|
+
identity = typeof callId === "string" ? `cursor-tool:${callId}` : undefined;
|
|
49
|
+
resolvedToolCall = mergeCursorToolCalls(options.startedToolCall, options.toolCall);
|
|
50
|
+
if (typeof callId === "string") {
|
|
51
|
+
options.onClearStartedCallId?.(callId);
|
|
52
|
+
options.ledger.clearStartedToolCall(callId);
|
|
53
|
+
}
|
|
54
|
+
resolvedToolCall = mergeShellOutputDeltasIntoCursorToolCall(
|
|
55
|
+
resolvedToolCall,
|
|
56
|
+
typeof callId === "string" ? options.shellOutput.takeDeltasForCall(callId) : undefined,
|
|
57
|
+
);
|
|
58
|
+
source = identity ? "started" : "fallback";
|
|
59
|
+
} else {
|
|
60
|
+
matchedStartedCallId = options.ledger.removeStartedToolCallForStep(options.toolCall, options.callId);
|
|
61
|
+
if (matchedStartedCallId) {
|
|
62
|
+
options.onClearStartedCallId?.(matchedStartedCallId);
|
|
63
|
+
}
|
|
64
|
+
resolvedToolCall = mergeShellOutputDeltasIntoCursorToolCall(
|
|
65
|
+
options.toolCall,
|
|
66
|
+
matchedStartedCallId ? options.shellOutput.takeDeltasForCall(matchedStartedCallId) : undefined,
|
|
67
|
+
);
|
|
68
|
+
const identityId = typeof options.callId === "string" ? options.callId : matchedStartedCallId;
|
|
69
|
+
identity = identityId ? `cursor-tool:${identityId}` : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.liveRun?.bridgeRun?.isBridgeMcpToolCall(resolvedToolCall)) {
|
|
73
|
+
const bridgeIdentity =
|
|
74
|
+
options.source === "step" && matchedStartedCallId ? `cursor-tool:${matchedStartedCallId}` : identity;
|
|
75
|
+
if (bridgeIdentity) options.ledger.recordCompletedIdentity(bridgeIdentity);
|
|
76
|
+
return { action: "ignore-bridge", identity: bridgeIdentity };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.source === "delta") {
|
|
80
|
+
return { action: "handle", toolCall: resolvedToolCall, identity, source };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
action: "handle",
|
|
84
|
+
toolCall: resolvedToolCall,
|
|
85
|
+
identity,
|
|
86
|
+
matchedStartedCallId,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { SendOptions } from "@cursor/sdk";
|
|
2
|
+
import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
|
|
3
|
+
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
4
|
+
import { getCursorAgentMessageOffset } from "./cursor-provider-turn-message-offset.js";
|
|
5
|
+
import type { installCursorSdkAbortErrorSuppression } from "./cursor-sdk-abort-error-guard.js";
|
|
6
|
+
import type {
|
|
7
|
+
CursorProviderTurnRunnerParams,
|
|
8
|
+
CursorProviderTurnPrepareResult,
|
|
9
|
+
CursorProviderTurnSendResult,
|
|
10
|
+
} from "./cursor-provider-turn-types.js";
|
|
11
|
+
import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
12
|
+
|
|
13
|
+
export interface SendCursorProviderTurnParams {
|
|
14
|
+
params: CursorProviderTurnRunnerParams;
|
|
15
|
+
prepared: CursorProviderTurnPrepareResult;
|
|
16
|
+
sdkEventDebug: CursorSdkEventDebugSink | undefined;
|
|
17
|
+
sdkAbortErrorSuppression: ReturnType<typeof installCursorSdkAbortErrorSuppression>;
|
|
18
|
+
throwIfAborted: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnParams): Promise<CursorProviderTurnSendResult> {
|
|
22
|
+
const { params, prepared, sdkEventDebug, sdkAbortErrorSuppression, throwIfAborted } = sendParams;
|
|
23
|
+
const { options } = params;
|
|
24
|
+
const { agent, cwd, payload, meta, runtime } = prepared;
|
|
25
|
+
const { turnCoordinator, liveRun } = runtime;
|
|
26
|
+
|
|
27
|
+
let completed = false;
|
|
28
|
+
let sdkRun: Awaited<ReturnType<typeof agent.send>> | null = null;
|
|
29
|
+
const abortListener = () => {
|
|
30
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
31
|
+
liveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
32
|
+
if (sdkRun) {
|
|
33
|
+
sdkRun.cancel().catch(() => {});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const abortSignal = options?.signal;
|
|
37
|
+
const abortRegistration = abortSignal
|
|
38
|
+
? { signal: abortSignal, listener: abortListener }
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
abortRegistration?.signal.addEventListener("abort", abortListener, { once: true });
|
|
43
|
+
throwIfAborted();
|
|
44
|
+
const cursorAgentMessageOffset = await getCursorAgentMessageOffset(agent.agentId, cwd, sdkEventDebug);
|
|
45
|
+
throwIfAborted();
|
|
46
|
+
sdkEventDebug?.recordSendMeta({
|
|
47
|
+
mode: meta.sendPlan.mode,
|
|
48
|
+
reason: meta.sendPlan.reason,
|
|
49
|
+
resetAgent: meta.sendPlan.resetAgent,
|
|
50
|
+
bootstrap: meta.bootstrap,
|
|
51
|
+
promptText: meta.prompt.text,
|
|
52
|
+
imageCount: meta.prompt.images.length,
|
|
53
|
+
useNativeToolReplay: meta.useNativeToolReplay,
|
|
54
|
+
bridgeEnabled: meta.bridgeEnabled,
|
|
55
|
+
nativeReplayId: meta.nativeReplayId,
|
|
56
|
+
promptInputTokens: meta.promptInputTokens,
|
|
57
|
+
agentMode: meta.agentMode,
|
|
58
|
+
});
|
|
59
|
+
sdkEventDebug?.recordSendPayload(payload);
|
|
60
|
+
sdkEventDebug?.recordProviderEvent("agent_send_start", payload);
|
|
61
|
+
const sendOptions: SendOptions = {
|
|
62
|
+
mode: meta.agentMode,
|
|
63
|
+
onDelta: (args) => {
|
|
64
|
+
sdkEventDebug?.recordOnDelta(args.update);
|
|
65
|
+
turnCoordinator.handleDelta(args.update);
|
|
66
|
+
},
|
|
67
|
+
onStep: (args) => {
|
|
68
|
+
sdkEventDebug?.recordOnStep(args.step);
|
|
69
|
+
turnCoordinator.handleStep(args.step);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const run = await agent.send(payload, sendOptions);
|
|
73
|
+
sdkRun = run;
|
|
74
|
+
sdkEventDebug?.recordRunMeta({
|
|
75
|
+
runId: run.id,
|
|
76
|
+
agentId: run.agentId,
|
|
77
|
+
status: run.status,
|
|
78
|
+
});
|
|
79
|
+
sdkEventDebug?.attachRunStream(run);
|
|
80
|
+
sdkEventDebug?.recordProviderEvent("agent_send_returned", {
|
|
81
|
+
runId: run.id,
|
|
82
|
+
agentId: run.agentId,
|
|
83
|
+
status: run.status,
|
|
84
|
+
});
|
|
85
|
+
if (liveRun) cursorLiveRuns.attachSdkRun(liveRun, run);
|
|
86
|
+
if (options?.signal?.aborted) {
|
|
87
|
+
sdkAbortErrorSuppression.suppressAbortErrors();
|
|
88
|
+
liveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
|
|
89
|
+
await run.cancel().catch(() => {});
|
|
90
|
+
throw new CursorLiveRunAbortError();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
completed = true;
|
|
94
|
+
return {
|
|
95
|
+
send: { run, cursorAgentMessageOffset },
|
|
96
|
+
abortRegistration,
|
|
97
|
+
};
|
|
98
|
+
} finally {
|
|
99
|
+
if (!completed && abortRegistration) {
|
|
100
|
+
abortRegistration.signal.removeEventListener("abort", abortRegistration.listener);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { InteractionUpdate } from "@cursor/sdk";
|
|
2
|
+
import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
|
|
3
|
+
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
4
|
+
|
|
5
|
+
export interface CursorShellOutputDelta {
|
|
6
|
+
stream: "stdout" | "stderr";
|
|
7
|
+
data: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CursorShellOutputDeltas {
|
|
11
|
+
stdout: string[];
|
|
12
|
+
stderr: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isCursorShellToolCall(toolCall: unknown): boolean {
|
|
16
|
+
return classifyCursorToolVisibility(toolCall).normalizedKey === "shell";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
|
|
20
|
+
if (update.type !== "shell-output-delta") return undefined;
|
|
21
|
+
const event = getField(update, "event");
|
|
22
|
+
const eventCase = getField(event, "case");
|
|
23
|
+
if (eventCase !== "stdout" && eventCase !== "stderr") return undefined;
|
|
24
|
+
const value = getField(event, "value");
|
|
25
|
+
const data = getField(value, "data");
|
|
26
|
+
if (typeof data !== "string" || data.length === 0) return undefined;
|
|
27
|
+
return { stream: eventCase, data };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mergeShellOutputDeltasIntoCursorToolCall(
|
|
31
|
+
toolCall: unknown,
|
|
32
|
+
deltas: CursorShellOutputDeltas | undefined,
|
|
33
|
+
): unknown {
|
|
34
|
+
if (!deltas) return toolCall;
|
|
35
|
+
const stdout = deltas.stdout.join("");
|
|
36
|
+
const stderr = deltas.stderr.join("");
|
|
37
|
+
if (!hasUsableText(stdout) && !hasUsableText(stderr)) return toolCall;
|
|
38
|
+
|
|
39
|
+
const toolRecord = asRecord(toolCall);
|
|
40
|
+
const result = getField(toolRecord, "result");
|
|
41
|
+
const resultRecord = asRecord(result);
|
|
42
|
+
if (!toolRecord || !resultRecord || resultRecord.status !== "success") return toolCall;
|
|
43
|
+
|
|
44
|
+
const value = getField(resultRecord, "value");
|
|
45
|
+
const valueRecord = asRecord(value);
|
|
46
|
+
const completedStdout = getField(valueRecord, "stdout");
|
|
47
|
+
const completedStderr = getField(valueRecord, "stderr");
|
|
48
|
+
if (hasUsableText(typeof completedStdout === "string" ? completedStdout : undefined)) return toolCall;
|
|
49
|
+
if (hasUsableText(typeof completedStderr === "string" ? completedStderr : undefined)) return toolCall;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...toolRecord,
|
|
53
|
+
result: {
|
|
54
|
+
...resultRecord,
|
|
55
|
+
value: {
|
|
56
|
+
...(valueRecord ?? {}),
|
|
57
|
+
stdout,
|
|
58
|
+
stderr,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class CursorShellOutputTracker {
|
|
65
|
+
private readonly activeShellCallIds = new Set<string>();
|
|
66
|
+
private readonly ambiguousShellOutputCallIds = new Set<string>();
|
|
67
|
+
private readonly shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
|
|
68
|
+
|
|
69
|
+
onShellToolStarted(callId: string): void {
|
|
70
|
+
this.activeShellCallIds.add(callId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onShellToolCleared(callId: string): void {
|
|
74
|
+
this.activeShellCallIds.delete(callId);
|
|
75
|
+
this.ambiguousShellOutputCallIds.delete(callId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
appendShellOutputDelta(delta: CursorShellOutputDelta): void {
|
|
79
|
+
if (this.activeShellCallIds.size !== 1) {
|
|
80
|
+
for (const activeCallId of this.activeShellCallIds) {
|
|
81
|
+
this.ambiguousShellOutputCallIds.add(activeCallId);
|
|
82
|
+
this.shellOutputDeltasByCallId.delete(activeCallId);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const [callId] = this.activeShellCallIds;
|
|
87
|
+
if (!callId || this.ambiguousShellOutputCallIds.has(callId)) return;
|
|
88
|
+
let deltas = this.shellOutputDeltasByCallId.get(callId);
|
|
89
|
+
if (!deltas) {
|
|
90
|
+
deltas = { stdout: [], stderr: [] };
|
|
91
|
+
this.shellOutputDeltasByCallId.set(callId, deltas);
|
|
92
|
+
}
|
|
93
|
+
deltas[delta.stream].push(delta.data);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
takeDeltasForCall(callId: string): CursorShellOutputDeltas | undefined {
|
|
97
|
+
const deltas = this.shellOutputDeltasByCallId.get(callId);
|
|
98
|
+
this.shellOutputDeltasByCallId.delete(callId);
|
|
99
|
+
return deltas;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
clear(): void {
|
|
103
|
+
this.activeShellCallIds.clear();
|
|
104
|
+
this.ambiguousShellOutputCallIds.clear();
|
|
105
|
+
this.shellOutputDeltasByCallId.clear();
|
|
106
|
+
}
|
|
107
|
+
}
|