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 { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
2
|
+
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
3
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
4
|
+
import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
|
|
5
|
+
import { resolveNativeReplayDisposition, type NativeReplayDisposition } from "./cursor-native-replay-routing.js";
|
|
6
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
7
|
+
import {
|
|
8
|
+
buildIncompleteCursorToolDisplay,
|
|
9
|
+
formatIncompleteCursorToolTrace,
|
|
10
|
+
type IncompleteCursorToolDiscardReason,
|
|
11
|
+
} from "./cursor-incomplete-tool-visibility.js";
|
|
12
|
+
import { scrubPiToolDisplay, scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
13
|
+
import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText } from "./cursor-tool-transcript.js";
|
|
14
|
+
import { getToolName } from "./cursor-transcript-utils.js";
|
|
15
|
+
import type { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
16
|
+
import type { CursorToolDisplaySource } from "./cursor-provider-turn-tool-ledger.js";
|
|
17
|
+
|
|
18
|
+
function formatCursorToolName(toolCall: unknown): string {
|
|
19
|
+
return truncateCursorDisplayLine(getToolName(toolCall), 80) || "unknown";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CursorTurnDisplayRouterOptions {
|
|
23
|
+
cwd: string;
|
|
24
|
+
resolvedApiKey?: string;
|
|
25
|
+
liveRun?: CursorLiveRun;
|
|
26
|
+
useNativeToolReplay: boolean;
|
|
27
|
+
activeToolNames?: ReadonlySet<string>;
|
|
28
|
+
nativeReplayId: string;
|
|
29
|
+
contentEmitter: CursorPartialContentEmitter;
|
|
30
|
+
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type CursorTurnDisplayAction =
|
|
34
|
+
| { kind: "queue_replay"; tool: ReturnType<typeof scrubPiToolDisplay>; replayToolId: string; disposition: NativeReplayDisposition }
|
|
35
|
+
| { kind: "emit_trace"; traceText: string; disposition: NativeReplayDisposition };
|
|
36
|
+
|
|
37
|
+
export class CursorTurnDisplayRouter {
|
|
38
|
+
private readonly cwd: string;
|
|
39
|
+
private readonly resolvedApiKey?: string;
|
|
40
|
+
private readonly liveRun?: CursorLiveRun;
|
|
41
|
+
private readonly useNativeToolReplay: boolean;
|
|
42
|
+
private readonly activeToolNames?: ReadonlySet<string>;
|
|
43
|
+
private readonly nativeReplayId: string;
|
|
44
|
+
private readonly contentEmitter: CursorPartialContentEmitter;
|
|
45
|
+
private readonly debugRecorder?: CursorSdkEventDebugRecorder;
|
|
46
|
+
private nativeToolDisplayCounter = 0;
|
|
47
|
+
nativeToolReplayStarted = false;
|
|
48
|
+
planTextCandidate: string | undefined;
|
|
49
|
+
|
|
50
|
+
constructor(options: CursorTurnDisplayRouterOptions) {
|
|
51
|
+
this.cwd = options.cwd;
|
|
52
|
+
this.resolvedApiKey = options.resolvedApiKey;
|
|
53
|
+
this.liveRun = options.liveRun;
|
|
54
|
+
this.useNativeToolReplay = options.useNativeToolReplay;
|
|
55
|
+
this.activeToolNames = options.activeToolNames;
|
|
56
|
+
this.nativeReplayId = options.nativeReplayId;
|
|
57
|
+
this.contentEmitter = options.contentEmitter;
|
|
58
|
+
this.debugRecorder = options.debugRecorder;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
routeCompletedToolCall(
|
|
62
|
+
toolCall: unknown,
|
|
63
|
+
options: { identity?: string; source?: CursorToolDisplaySource } = {},
|
|
64
|
+
): CursorTurnDisplayAction | undefined {
|
|
65
|
+
const planText = getCursorCreatePlanText(toolCall);
|
|
66
|
+
if (planText) this.planTextCandidate = scrubSensitiveText(planText, this.resolvedApiKey);
|
|
67
|
+
|
|
68
|
+
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd: this.cwd }), this.resolvedApiKey);
|
|
69
|
+
const display = buildCursorPiToolDisplay(toolCall, { cwd: this.cwd });
|
|
70
|
+
const disposition = resolveNativeReplayDisposition({
|
|
71
|
+
toolName: display.toolName,
|
|
72
|
+
useNativeToolReplay: this.useNativeToolReplay,
|
|
73
|
+
activeToolNames: this.activeToolNames,
|
|
74
|
+
hasLiveRun: this.liveRun !== undefined,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (disposition === "queue_replay" && this.liveRun) {
|
|
78
|
+
this.nativeToolReplayStarted = true;
|
|
79
|
+
const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
|
|
80
|
+
const scrubbedDisplay = scrubPiToolDisplay(display, this.resolvedApiKey);
|
|
81
|
+
this.recordDisplayDecision({
|
|
82
|
+
action: "queue_replay",
|
|
83
|
+
disposition,
|
|
84
|
+
toolName: display.toolName,
|
|
85
|
+
identity: options.identity,
|
|
86
|
+
source: options.source,
|
|
87
|
+
transcript,
|
|
88
|
+
replayToolId: id,
|
|
89
|
+
});
|
|
90
|
+
return { kind: "queue_replay", tool: scrubbedDisplay, replayToolId: id, disposition };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const traceText =
|
|
94
|
+
disposition === "inactive_trace"
|
|
95
|
+
? formatInactiveCursorReplayTrace(scrubPiToolDisplay(display, this.resolvedApiKey))
|
|
96
|
+
: transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`;
|
|
97
|
+
this.recordDisplayDecision({
|
|
98
|
+
action: "emit_trace",
|
|
99
|
+
disposition,
|
|
100
|
+
toolName: display.toolName,
|
|
101
|
+
identity: options.identity,
|
|
102
|
+
source: options.source,
|
|
103
|
+
transcript,
|
|
104
|
+
traceText,
|
|
105
|
+
});
|
|
106
|
+
return { kind: "emit_trace", traceText, disposition };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
routeIncompleteStartedToolCall(
|
|
110
|
+
toolCall: unknown,
|
|
111
|
+
reason: IncompleteCursorToolDiscardReason,
|
|
112
|
+
): CursorTurnDisplayAction | undefined {
|
|
113
|
+
const display = scrubPiToolDisplay(
|
|
114
|
+
buildIncompleteCursorToolDisplay(toolCall, reason, { apiKey: this.resolvedApiKey }),
|
|
115
|
+
this.resolvedApiKey,
|
|
116
|
+
);
|
|
117
|
+
const disposition = resolveNativeReplayDisposition({
|
|
118
|
+
toolName: display.toolName,
|
|
119
|
+
useNativeToolReplay: this.useNativeToolReplay,
|
|
120
|
+
activeToolNames: this.activeToolNames,
|
|
121
|
+
hasLiveRun: this.liveRun !== undefined,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (disposition === "queue_replay" && this.liveRun && reason !== "abort") {
|
|
125
|
+
this.nativeToolReplayStarted = true;
|
|
126
|
+
const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
|
|
127
|
+
this.recordDisplayDecision({
|
|
128
|
+
action: "queue_replay",
|
|
129
|
+
disposition,
|
|
130
|
+
toolName: display.toolName,
|
|
131
|
+
source: "started",
|
|
132
|
+
reason: "incomplete-started-tool-call",
|
|
133
|
+
replayToolId: id,
|
|
134
|
+
});
|
|
135
|
+
return { kind: "queue_replay", tool: display, replayToolId: id, disposition };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const traceText =
|
|
139
|
+
disposition === "inactive_trace"
|
|
140
|
+
? formatInactiveCursorReplayTrace(display)
|
|
141
|
+
: formatIncompleteCursorToolTrace(display);
|
|
142
|
+
this.recordDisplayDecision({
|
|
143
|
+
action: "emit_trace",
|
|
144
|
+
disposition,
|
|
145
|
+
toolName: display.toolName,
|
|
146
|
+
source: "started",
|
|
147
|
+
reason: "incomplete-started-tool-call",
|
|
148
|
+
traceText,
|
|
149
|
+
});
|
|
150
|
+
return { kind: "emit_trace", traceText, disposition };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
emitDisplayAction(action: CursorTurnDisplayAction): void {
|
|
154
|
+
if (action.kind === "queue_replay") {
|
|
155
|
+
if (!this.liveRun) return;
|
|
156
|
+
cursorLiveRuns.queueEvent(this.liveRun, {
|
|
157
|
+
type: "tool",
|
|
158
|
+
tool: { ...action.tool, id: action.replayToolId },
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.emitCursorToolTrace(action.traceText);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
recordIgnoreBridgeDecision(
|
|
166
|
+
identity: string | undefined,
|
|
167
|
+
toolName: string,
|
|
168
|
+
source: "delta" | "step",
|
|
169
|
+
): void {
|
|
170
|
+
this.debugRecorder?.recordDisplayDecision({
|
|
171
|
+
action: "ignore-bridge",
|
|
172
|
+
toolName,
|
|
173
|
+
identity,
|
|
174
|
+
source,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
recordDuplicateSkip(
|
|
179
|
+
toolName: string,
|
|
180
|
+
options: { identity?: string; source?: CursorToolDisplaySource; reason: string },
|
|
181
|
+
): void {
|
|
182
|
+
this.recordDisplayDecision({
|
|
183
|
+
action: "skip-duplicate",
|
|
184
|
+
toolName,
|
|
185
|
+
identity: options.identity,
|
|
186
|
+
source: options.source,
|
|
187
|
+
reason: options.reason,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
recordIncompleteSkip(
|
|
192
|
+
toolName: string,
|
|
193
|
+
reason: string,
|
|
194
|
+
): void {
|
|
195
|
+
this.recordDisplayDecision({
|
|
196
|
+
action: "skip-incomplete-fast-local",
|
|
197
|
+
toolName,
|
|
198
|
+
source: "started",
|
|
199
|
+
reason,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private recordDisplayDecision(decision: Parameters<CursorSdkEventDebugRecorder["recordDisplayDecision"]>[0]): void {
|
|
204
|
+
this.debugRecorder?.recordDisplayDecision(decision);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private emitCursorToolTrace(text: string): void {
|
|
208
|
+
const traceText = text.endsWith("\n") ? text : `${text}\n`;
|
|
209
|
+
if (this.liveRun) {
|
|
210
|
+
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: traceText });
|
|
211
|
+
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-completed" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.contentEmitter.appendThinkingBlock(traceText);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
|
|
2
|
+
import {
|
|
3
|
+
drainCursorLiveRunTurn,
|
|
4
|
+
flushPendingCursorLiveRunTraceEventsToStream,
|
|
5
|
+
settleCursorLiveToolBatch,
|
|
6
|
+
} from "./cursor-provider-live-run-drain.js";
|
|
7
|
+
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
8
|
+
import {
|
|
9
|
+
buildIncompleteCursorToolRunOutcome,
|
|
10
|
+
type IncompleteCursorToolRunOutcomeInput,
|
|
11
|
+
} from "./cursor-incomplete-tool-visibility.js";
|
|
12
|
+
import type {
|
|
13
|
+
CursorProviderTurnPrepareResult,
|
|
14
|
+
CursorProviderTurnRunnerParams,
|
|
15
|
+
} from "./cursor-provider-turn-types.js";
|
|
16
|
+
import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
17
|
+
|
|
18
|
+
export interface EmitCursorLiveTurnParams {
|
|
19
|
+
params: CursorProviderTurnRunnerParams;
|
|
20
|
+
prepared: CursorProviderTurnPrepareResult;
|
|
21
|
+
sdkEventDebug: CursorSdkEventDebugSink | undefined;
|
|
22
|
+
discardIncompleteTools: (outcome: IncompleteCursorToolRunOutcomeInput) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function emitCursorLiveTurn(emitParams: EmitCursorLiveTurnParams): Promise<void> {
|
|
26
|
+
const { params, prepared, sdkEventDebug, discardIncompleteTools } = emitParams;
|
|
27
|
+
if (prepared.runtime.kind !== "live") throw new Error("emitCursorLiveTurn requires a live run");
|
|
28
|
+
const { liveRun, turnCoordinator } = prepared.runtime;
|
|
29
|
+
|
|
30
|
+
const { options, model } = params;
|
|
31
|
+
try {
|
|
32
|
+
await cursorLiveRuns.withRunLease(liveRun, options?.signal, async () => {
|
|
33
|
+
await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
|
|
34
|
+
await settleCursorLiveToolBatch(liveRun);
|
|
35
|
+
turnCoordinator.closeTraceBlock();
|
|
36
|
+
await drainCursorLiveRunTurn(params.stream, params.partial, model, params.context, liveRun, 0, {
|
|
37
|
+
mode: "emit",
|
|
38
|
+
signal: options?.signal,
|
|
39
|
+
debugRecorder: sdkEventDebug,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
} catch (caught) {
|
|
43
|
+
if (caught instanceof CursorLiveRunAbortError) {
|
|
44
|
+
discardIncompleteTools({ status: "cancelled", signalAborted: true });
|
|
45
|
+
turnCoordinator.closeTraceBlock();
|
|
46
|
+
flushPendingCursorLiveRunTraceEventsToStream(params.stream, params.partial, liveRun, {
|
|
47
|
+
includeTracesBehindQueuedTools: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
throw caught;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function discardIncompleteToolsFromPrepared(
|
|
55
|
+
prepared: CursorProviderTurnPrepareResult | undefined,
|
|
56
|
+
outcome: IncompleteCursorToolRunOutcomeInput,
|
|
57
|
+
): void {
|
|
58
|
+
prepared?.runtime.turnCoordinator.discardIncompleteStartedToolCalls(buildIncompleteCursorToolRunOutcome(outcome));
|
|
59
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createAgentPlatform } from "@cursor/sdk";
|
|
2
|
+
import type { SDKAgent } from "@cursor/sdk";
|
|
3
|
+
import { loadCursorTranscriptWebToolCallsAfterOffset } from "./cursor-agent-message-web-tools.js";
|
|
4
|
+
import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
|
|
5
|
+
import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
6
|
+
import type { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
7
|
+
import {
|
|
8
|
+
isCursorRunFinishedSuccessfully,
|
|
9
|
+
resolveCursorRunOutcome,
|
|
10
|
+
type CursorRunOutcome,
|
|
11
|
+
} from "./cursor-provider-run-outcome.js";
|
|
12
|
+
import type { CursorProviderTurnPrepareResult } from "./cursor-provider-turn-types.js";
|
|
13
|
+
|
|
14
|
+
export async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
const platform = await createAgentPlatform();
|
|
17
|
+
const checkpoint = await platform.checkpointStore.loadLatest(agentId);
|
|
18
|
+
const contextWindow = getCheckpointContextWindow(checkpoint);
|
|
19
|
+
if (contextWindow) saveCachedContextWindow(modelId, contextWindow);
|
|
20
|
+
} catch {
|
|
21
|
+
// Context-window cache failures must not affect response streaming.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BuildCursorRunOutcomeParams {
|
|
26
|
+
waitResult: Awaited<ReturnType<Awaited<ReturnType<SDKAgent["send"]>>["wait"]>>;
|
|
27
|
+
prepared: CursorProviderTurnPrepareResult;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
runResultFallback?: string;
|
|
30
|
+
resolvedApiKey?: string;
|
|
31
|
+
optionsApiKey?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildCursorRunOutcomeFromWait(params: BuildCursorRunOutcomeParams): CursorRunOutcome {
|
|
35
|
+
const { waitResult, prepared } = params;
|
|
36
|
+
const { turnCoordinator, liveRun } = prepared.runtime;
|
|
37
|
+
const { textDeltas } = prepared;
|
|
38
|
+
return resolveCursorRunOutcome({
|
|
39
|
+
waitResult,
|
|
40
|
+
signalAborted: params.signal?.aborted,
|
|
41
|
+
textDeltas: liveRun?.textDeltas ?? textDeltas,
|
|
42
|
+
emittedText: liveRun?.emittedText ?? textDeltas.join(""),
|
|
43
|
+
planTextCandidate: turnCoordinator.planTextCandidate,
|
|
44
|
+
selectFinalTextOptions: liveRun ? undefined : { allowPartialPrefix: true },
|
|
45
|
+
runResultFallback: params.runResultFallback,
|
|
46
|
+
resolvedApiKey: params.resolvedApiKey,
|
|
47
|
+
optionsApiKey: params.optionsApiKey,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function replayCursorTranscriptWebToolCalls(
|
|
52
|
+
agentId: string,
|
|
53
|
+
cwd: string,
|
|
54
|
+
messageOffset: number | undefined,
|
|
55
|
+
turnCoordinator: CursorSdkTurnCoordinator,
|
|
56
|
+
sdkEventDebug: CursorSdkEventDebugSink | undefined,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
const transcriptToolCalls = await loadCursorTranscriptWebToolCallsAfterOffset({
|
|
60
|
+
agentId,
|
|
61
|
+
cwd,
|
|
62
|
+
offset: messageOffset,
|
|
63
|
+
});
|
|
64
|
+
if (transcriptToolCalls.length === 0) return;
|
|
65
|
+
sdkEventDebug?.recordCoordinatorEvent("cursor-transcript-web-tools", {
|
|
66
|
+
agentId,
|
|
67
|
+
messageOffset,
|
|
68
|
+
count: transcriptToolCalls.length,
|
|
69
|
+
});
|
|
70
|
+
turnCoordinator.handleTranscriptCompletedToolCalls(transcriptToolCalls);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
sdkEventDebug?.recordError("cursor_transcript_web_tools", error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface AwaitFinalizeCursorRunOutcomeParams {
|
|
77
|
+
run: Awaited<ReturnType<SDKAgent["send"]>>;
|
|
78
|
+
prepared: CursorProviderTurnPrepareResult;
|
|
79
|
+
cursorAgentMessageOffset: number | undefined;
|
|
80
|
+
modelId: string;
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
runResultFallback?: string;
|
|
83
|
+
resolvedApiKey?: string;
|
|
84
|
+
optionsApiKey?: string;
|
|
85
|
+
sdkEventDebug?: CursorSdkEventDebugSink;
|
|
86
|
+
waitResult?: Awaited<ReturnType<Awaited<ReturnType<SDKAgent["send"]>>["wait"]>>;
|
|
87
|
+
cacheContextWindow?: boolean;
|
|
88
|
+
/** Session agent id for checkpoint cache; defaults to run.agentId when omitted. */
|
|
89
|
+
contextWindowAgentId?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Single wait/finalize path for SDK runs: wait, debug capture, transcript replay, incomplete tools, artifacts, context cache. */
|
|
93
|
+
export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorRunOutcomeParams): Promise<CursorRunOutcome> {
|
|
94
|
+
const waitResult = params.waitResult ?? (await params.run.wait());
|
|
95
|
+
params.sdkEventDebug?.recordWaitResult(waitResult);
|
|
96
|
+
const outcome = buildCursorRunOutcomeFromWait({
|
|
97
|
+
waitResult,
|
|
98
|
+
prepared: params.prepared,
|
|
99
|
+
signal: params.signal,
|
|
100
|
+
runResultFallback: params.runResultFallback,
|
|
101
|
+
resolvedApiKey: params.resolvedApiKey,
|
|
102
|
+
optionsApiKey: params.optionsApiKey,
|
|
103
|
+
});
|
|
104
|
+
if (isCursorRunFinishedSuccessfully(outcome)) {
|
|
105
|
+
await replayCursorTranscriptWebToolCalls(
|
|
106
|
+
params.run.agentId,
|
|
107
|
+
params.prepared.cwd,
|
|
108
|
+
params.cursorAgentMessageOffset,
|
|
109
|
+
params.prepared.runtime.turnCoordinator,
|
|
110
|
+
params.sdkEventDebug,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
params.prepared.runtime.turnCoordinator.discardIncompleteStartedToolCalls(outcome.incompleteTools);
|
|
114
|
+
await params.sdkEventDebug?.captureRunArtifacts(params.run);
|
|
115
|
+
if (params.cacheContextWindow !== false) {
|
|
116
|
+
await cacheSdkContextWindow(params.contextWindowAgentId ?? params.run.agentId, params.modelId);
|
|
117
|
+
}
|
|
118
|
+
return outcome;
|
|
119
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AssistantMessage, AssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
3
|
+
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
4
|
+
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
5
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
6
|
+
import {
|
|
7
|
+
CURSOR_TOOL_LIFECYCLE_DEFER_MS,
|
|
8
|
+
formatCursorToolLifecycleProgressText,
|
|
9
|
+
isCursorToolLifecycleEligible,
|
|
10
|
+
} from "./cursor-tool-lifecycle.js";
|
|
11
|
+
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
12
|
+
|
|
13
|
+
function getNormalizedCursorToolName(toolCall: unknown): string {
|
|
14
|
+
return classifyCursorToolVisibility(toolCall).normalizedName;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CursorToolLifecycleEmitterOptions {
|
|
18
|
+
liveRun?: CursorLiveRun;
|
|
19
|
+
resolvedApiKey?: string;
|
|
20
|
+
contentEmitter: CursorPartialContentEmitter;
|
|
21
|
+
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
22
|
+
hasStartedToolCall: (callId: string) => boolean;
|
|
23
|
+
isBridgeMcpToolCall: (toolCall: unknown) => boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CursorToolLifecycleEmitter {
|
|
27
|
+
private readonly liveRun?: CursorLiveRun;
|
|
28
|
+
private readonly resolvedApiKey?: string;
|
|
29
|
+
private readonly contentEmitter: CursorPartialContentEmitter;
|
|
30
|
+
private readonly debugRecorder?: CursorSdkEventDebugRecorder;
|
|
31
|
+
private readonly hasStartedToolCall: (callId: string) => boolean;
|
|
32
|
+
private readonly isBridgeMcpToolCall: (toolCall: unknown) => boolean;
|
|
33
|
+
private readonly emittedLifecycleCallIds = new Set<string>();
|
|
34
|
+
private readonly lifecycleTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
35
|
+
|
|
36
|
+
constructor(options: CursorToolLifecycleEmitterOptions) {
|
|
37
|
+
this.liveRun = options.liveRun;
|
|
38
|
+
this.resolvedApiKey = options.resolvedApiKey;
|
|
39
|
+
this.contentEmitter = options.contentEmitter;
|
|
40
|
+
this.debugRecorder = options.debugRecorder;
|
|
41
|
+
this.hasStartedToolCall = options.hasStartedToolCall;
|
|
42
|
+
this.isBridgeMcpToolCall = options.isBridgeMcpToolCall;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
maybeSchedule(callId: unknown, toolCall: unknown): void {
|
|
46
|
+
if (typeof callId !== "string" || this.emittedLifecycleCallIds.has(callId)) return;
|
|
47
|
+
if (this.isBridgeMcpToolCall(toolCall)) return;
|
|
48
|
+
if (!isCursorToolLifecycleEligible(toolCall)) return;
|
|
49
|
+
|
|
50
|
+
this.cancel(callId);
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
this.lifecycleTimers.delete(callId);
|
|
53
|
+
if (!this.hasStartedToolCall(callId)) return;
|
|
54
|
+
if (this.emittedLifecycleCallIds.has(callId)) return;
|
|
55
|
+
this.emit(callId, toolCall);
|
|
56
|
+
}, CURSOR_TOOL_LIFECYCLE_DEFER_MS);
|
|
57
|
+
timer.unref?.();
|
|
58
|
+
this.lifecycleTimers.set(callId, timer);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cancel(callId: string): void {
|
|
62
|
+
const timer = this.lifecycleTimers.get(callId);
|
|
63
|
+
if (!timer) return;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
this.lifecycleTimers.delete(callId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clear(): void {
|
|
69
|
+
this.emittedLifecycleCallIds.clear();
|
|
70
|
+
for (const timer of this.lifecycleTimers.values()) clearTimeout(timer);
|
|
71
|
+
this.lifecycleTimers.clear();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private emit(callId: string, toolCall: unknown): void {
|
|
75
|
+
const progressText = formatCursorToolLifecycleProgressText(toolCall, this.resolvedApiKey);
|
|
76
|
+
if (!progressText) return;
|
|
77
|
+
this.emittedLifecycleCallIds.add(callId);
|
|
78
|
+
this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle", {
|
|
79
|
+
callId,
|
|
80
|
+
toolName: getNormalizedCursorToolName(toolCall),
|
|
81
|
+
progressText,
|
|
82
|
+
liveRun: this.liveRun !== undefined,
|
|
83
|
+
});
|
|
84
|
+
if (this.liveRun) {
|
|
85
|
+
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: progressText });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.contentEmitter.appendThinkingDelta(progressText);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createTurnCoordinatorContentEmitter(
|
|
93
|
+
stream: AssistantMessageEventStream,
|
|
94
|
+
partial: AssistantMessage,
|
|
95
|
+
): CursorPartialContentEmitter {
|
|
96
|
+
return new CursorPartialContentEmitter(stream, partial, undefined, false);
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { countCursorAgentMessages } from "./cursor-agent-message-web-tools.js";
|
|
2
|
+
import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
3
|
+
|
|
4
|
+
export async function getCursorAgentMessageOffset(
|
|
5
|
+
agentId: string,
|
|
6
|
+
cwd: string,
|
|
7
|
+
sdkEventDebug: CursorSdkEventDebugSink | undefined,
|
|
8
|
+
): Promise<number | undefined> {
|
|
9
|
+
try {
|
|
10
|
+
return await countCursorAgentMessages(agentId, cwd);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
sdkEventDebug?.recordError("cursor_agent_message_count", error);
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|