@emmaneugene/pi-cursor-sdk 0.4.1 → 0.4.3
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 +12 -0
- package/README.md +2 -0
- package/dist/cursor-live-run-coordinator.js +11 -0
- package/dist/cursor-pi-tool-bridge-run.js +3 -0
- package/dist/cursor-pi-tool-bridge.js +28 -12
- package/dist/cursor-provider-lazy.js +2 -2
- package/dist/cursor-provider-live-run-drain.js +4 -4
- package/dist/cursor-provider-run-finalizer.js +1 -0
- package/dist/cursor-provider-runtime-context.js +1 -0
- package/dist/cursor-provider-turn-prepare.js +17 -9
- package/dist/cursor-provider-turn-runner.js +5 -4
- package/dist/cursor-provider.js +3 -2
- package/dist/cursor-session-agent.js +9 -7
- package/dist/index.js +45 -6
- package/docs/cursor-model-ux-spec.md +1 -1
- package/docs/cursor-testing-lessons.md +15 -6
- package/package.json +1 -1
- package/src/cursor-live-run-coordinator.ts +11 -0
- package/src/cursor-pi-tool-bridge-run.ts +4 -0
- package/src/cursor-pi-tool-bridge-types.ts +1 -0
- package/src/cursor-pi-tool-bridge.ts +33 -12
- package/src/cursor-provider-lazy.ts +3 -1
- package/src/cursor-provider-live-run-drain.ts +4 -3
- package/src/cursor-provider-run-finalizer.ts +1 -0
- package/src/cursor-provider-runtime-context.ts +13 -0
- package/src/cursor-provider-turn-prepare.ts +20 -9
- package/src/cursor-provider-turn-runner.ts +13 -4
- package/src/cursor-provider-turn-types.ts +2 -0
- package/src/cursor-provider.ts +4 -1
- package/src/cursor-session-agent.ts +11 -7
- package/src/index.ts +57 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3 - 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Keep live Cursor runs open while bridged pi tools await results. Long `cursor_ask_question` waits and other bridge calls now use the bridge CallTool deadline instead of being cancelled by the five-minute inactive-run cleanup.
|
|
8
|
+
|
|
9
|
+
## 0.4.2 - 2026-09-07
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Register an isolated Cursor provider, SDK agent scope, and pi tool bridge for nested in-process child sessions. A non-Cursor parent can now dispatch a pi subagent with a Cursor model, and a Cursor parent can dispatch a Cursor-model pi subagent without waiting on or disposing its own busy Cursor SDK agent.
|
|
14
|
+
|
|
3
15
|
## 0.4.1 - 2026-09-07
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -367,6 +367,8 @@ Local Cursor runs use two separate tool surfaces:
|
|
|
367
367
|
|
|
368
368
|
Bridge capabilities are snapshotted from `pi.getActiveTools()` and `pi.getAllTools()` for each Cursor run, including per-tool prompt guidelines when pi exposes them. Cursor sees active bridgeable pi tools as collision-safe MCP names such as `pi__sem_reindex` only when they are exposed in that current run. When exposed, Cursor is instructed to prefer `pi__mcp` for MCP work and `pi__subagent` for delegation; Cursor-configured MCP and Cursor-native subagents are fallbacks when the matching pi tool is not exposed or is unavailable. Pi session output, tool cards, confirmations, hooks, renderers, history, and abort behavior use the real pi tool name, such as `sem_reindex`. The bridge queues Cursor's MCP call, emits a normal pi `toolCall`, waits for the matching pi `toolResult`, and resolves that result back into the same live Cursor SDK run without creating a new `Agent`, unless the run was disposed, aborted, or cancelled. The bridge does not call pi tool `execute()` handlers directly.
|
|
369
369
|
|
|
370
|
+
Pi subagents can select Cursor models under both Cursor and non-Cursor parents. Each nested Cursor child registers a child-local provider and bridge and uses an isolated SDK agent scope, so it does not replace or wait on the parent's active Cursor agent.
|
|
371
|
+
|
|
370
372
|
Overlapping built-in pi tools (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) are hidden by default because Cursor local agents already have native equivalents. Extension/custom tools and non-overlapping active tools present in pi's active tool registry normally remain exposed. The bridge also exposes `cursor_ask_question` as `pi__cursor_ask_question` when enabled, allowing Cursor to ask the user through pi UI instead of silently choosing a default. When pi has visible Agent Skills loaded, the extension rewrites pi's skill catalog for Cursor and exposes `cursor_activate_skill` as `pi__cursor_activate_skill`; Cursor should call that bridge tool with a listed skill name to load the full `SKILL.md` and bundled resource list before applying the skill. If the bridge is disabled, the catalog remains available and instructs Cursor to fall back to reading the listed `SKILL.md` path directly.
|
|
371
373
|
|
|
372
374
|
Cursor-native tool replay is separate from the bridge. Replay cards are display-only recorded Cursor SDK activity. They never re-run Cursor-side commands, reapply Cursor edits, call MCP servers, or mutate pi state. See [Cursor native tool replay](docs/cursor-native-tool-replay.md).
|
|
@@ -11,6 +11,11 @@ function isPendingBridgeToolRequest(run, request) {
|
|
|
11
11
|
const bridgeRun = [run.bridgeRun, run.sessionBridgeRun].find((candidate) => candidate?.id === request.runId);
|
|
12
12
|
return bridgeRun?.hasPendingPiToolCallId(request.piToolCallId) === true;
|
|
13
13
|
}
|
|
14
|
+
function hasPendingBridgeCalls(run) {
|
|
15
|
+
if (run.bridgeRun?.hasPendingCalls())
|
|
16
|
+
return true;
|
|
17
|
+
return run.sessionBridgeRun !== run.bridgeRun && run.sessionBridgeRun?.hasPendingCalls() === true;
|
|
18
|
+
}
|
|
14
19
|
async function cancelCursorLiveSdkRun(run) {
|
|
15
20
|
if (!run.sdkRun)
|
|
16
21
|
return;
|
|
@@ -350,6 +355,12 @@ export function createCursorLiveRunCoordinator(deps) {
|
|
|
350
355
|
return;
|
|
351
356
|
state.idleDisposeRequested = false;
|
|
352
357
|
state.idleDisposeTimer = setTimeout(() => {
|
|
358
|
+
if (hasPendingBridgeCalls(run)) {
|
|
359
|
+
// Bridge calls have their own deadline. Reuse the idle interval to avoid a hot
|
|
360
|
+
// polling loop if the SDK does not emit a terminal event after that deadline.
|
|
361
|
+
coordinator.requestIdleDispose(run);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
353
364
|
void coordinator.release(run).catch(() => {
|
|
354
365
|
// Idle dispose must not leave release failures as unhandled rejections.
|
|
355
366
|
});
|
|
@@ -126,6 +126,9 @@ export class CursorPiToolBridgeRunImpl {
|
|
|
126
126
|
async resolveToolResultsFromContext(context) {
|
|
127
127
|
await this.resolveToolResults(context.messages.map(asToolResultMessage).filter((message) => message !== undefined));
|
|
128
128
|
}
|
|
129
|
+
hasPendingCalls() {
|
|
130
|
+
return this.pendingCount() > 0;
|
|
131
|
+
}
|
|
129
132
|
hasPendingPiToolCallId(piToolCallId) {
|
|
130
133
|
return this.pendingByPiToolCallId.has(piToolCallId);
|
|
131
134
|
}
|
|
@@ -48,18 +48,9 @@ Get-CimInstance Win32_Process -Filter "Name = 'bash.exe' OR Name = 'sh.exe'" |
|
|
|
48
48
|
windowsHide: true,
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
// Replacing a bridge during a live MCP run cancels its pending pi tool
|
|
53
|
-
// calls. Keep the active registry as a final safety belt.
|
|
54
|
-
if (registeredCursorPiToolBridge?.hasLiveRuns()) {
|
|
55
|
-
return registeredCursorPiToolBridge;
|
|
56
|
-
}
|
|
57
|
-
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
|
|
58
|
-
void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
|
|
59
|
-
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
60
|
-
registeredCursorPiToolBridge = bridge;
|
|
51
|
+
function attachCursorPiToolBridgeHandlers(pi, bridge, options) {
|
|
61
52
|
pi.on("tool_call", (event, ctx) => {
|
|
62
|
-
if (
|
|
53
|
+
if (!options.isActive())
|
|
63
54
|
return undefined;
|
|
64
55
|
if (!bridge.hasPendingPiToolCallId(event.toolCallId)) {
|
|
65
56
|
return isCursorPiBridgeToolCallId(event.toolCallId)
|
|
@@ -86,9 +77,34 @@ export function registerCursorPiToolBridge(pi) {
|
|
|
86
77
|
});
|
|
87
78
|
pi.on("session_shutdown", async (event) => {
|
|
88
79
|
const reason = `Cursor pi tool bridge session shutdown: ${event.reason}`;
|
|
89
|
-
|
|
80
|
+
if (options.abortAllOnShutdown)
|
|
81
|
+
bridgeToolExecutionAbortTracker.abortAll(reason);
|
|
90
82
|
await bridge.disposeAll(reason);
|
|
91
83
|
});
|
|
84
|
+
}
|
|
85
|
+
export function registerCursorPiToolBridge(pi) {
|
|
86
|
+
// Replacing a bridge during a live MCP run cancels its pending pi tool
|
|
87
|
+
// calls. Keep the active registry as a final safety belt.
|
|
88
|
+
if (registeredCursorPiToolBridge?.hasLiveRuns()) {
|
|
89
|
+
return registeredCursorPiToolBridge;
|
|
90
|
+
}
|
|
91
|
+
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
|
|
92
|
+
void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
|
|
93
|
+
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
94
|
+
registeredCursorPiToolBridge = bridge;
|
|
95
|
+
attachCursorPiToolBridgeHandlers(pi, bridge, {
|
|
96
|
+
abortAllOnShutdown: true,
|
|
97
|
+
isActive: () => registeredCursorPiToolBridge === bridge,
|
|
98
|
+
});
|
|
99
|
+
return bridge;
|
|
100
|
+
}
|
|
101
|
+
/** Register a bridge owned only by one nested in-process child session. */
|
|
102
|
+
export function registerNestedCursorPiToolBridge(pi) {
|
|
103
|
+
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
104
|
+
attachCursorPiToolBridgeHandlers(pi, bridge, {
|
|
105
|
+
abortAllOnShutdown: false,
|
|
106
|
+
isActive: () => true,
|
|
107
|
+
});
|
|
92
108
|
return bridge;
|
|
93
109
|
}
|
|
94
110
|
export function getRegisteredCursorPiToolBridge() {
|
|
@@ -21,11 +21,11 @@ function makeProviderRuntimeErrorMessage(model, error, apiKey) {
|
|
|
21
21
|
errorMessage: `Cursor provider runtime failed: ${sanitizeCursorProviderError(error, apiKey)}`,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
export function streamCursorLazy(model, context, options) {
|
|
24
|
+
export function streamCursorLazy(model, context, options, runtimeContext) {
|
|
25
25
|
const outer = createAssistantMessageEventStream();
|
|
26
26
|
queueMicrotask(async () => {
|
|
27
27
|
try {
|
|
28
|
-
for await (const event of streamCursor(model, context, options)) {
|
|
28
|
+
for await (const event of streamCursor(model, context, options, runtimeContext)) {
|
|
29
29
|
outer.push(event);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -32,8 +32,8 @@ function getCursorNativeReplayIdFromToolCallId(toolCallId) {
|
|
|
32
32
|
export function getPendingCursorLiveRun(context) {
|
|
33
33
|
return cursorLiveRuns.getPendingFromContext(context, getCursorNativeReplayIdFromToolCallId);
|
|
34
34
|
}
|
|
35
|
-
export function
|
|
36
|
-
return cursorLiveRuns.getActiveForScope();
|
|
35
|
+
export function getActiveCursorLiveRunForScope(scopeKey) {
|
|
36
|
+
return cursorLiveRuns.getActiveForScope(scopeKey);
|
|
37
37
|
}
|
|
38
38
|
function splitTextIntoReplayDeltas(text) {
|
|
39
39
|
const deltas = [];
|
|
@@ -334,10 +334,10 @@ export async function drainCursorLiveRunTurn(stream, partial, model, context, ru
|
|
|
334
334
|
});
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
-
export async function drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, signal, turnDebugRecorder) {
|
|
337
|
+
export async function drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, signal, turnDebugRecorder, scopeKey) {
|
|
338
338
|
turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
|
|
339
339
|
while (true) {
|
|
340
|
-
const run = getPendingCursorLiveRun(context) ??
|
|
340
|
+
const run = getPendingCursorLiveRun(context) ?? getActiveCursorLiveRunForScope(scopeKey);
|
|
341
341
|
if (!run || run.disposed) {
|
|
342
342
|
turnDebugRecorder?.recordDrainEvent("pre_send_end", { outcome: "continue_send", reason: "no_pending_run" });
|
|
343
343
|
return "continue_send";
|
|
@@ -89,6 +89,7 @@ export class CursorRunFinalizer {
|
|
|
89
89
|
if (liveCompletion) {
|
|
90
90
|
void liveCompletion.waitCompletion
|
|
91
91
|
.finally(async () => {
|
|
92
|
+
await prepared?.lifecycle.dispose().catch(() => { });
|
|
92
93
|
await this.finalizeSdkEventDebugBestEffort();
|
|
93
94
|
this.safeCleanup(() => this.params.sdkProcessErrorGuard.dispose());
|
|
94
95
|
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -20,20 +20,23 @@ import { MISSING_CURSOR_API_KEY_MESSAGE } from "./cursor-provider-errors.js";
|
|
|
20
20
|
import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
21
21
|
import { resolveCursorApiKey } from "./cursor-api-key.js";
|
|
22
22
|
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
23
|
-
export function resolveCursorProviderTurnConfig(cwd) {
|
|
24
|
-
return resolveEffectiveCursorConfig({ cwd, projectTrusted
|
|
23
|
+
export function resolveCursorProviderTurnConfig(cwd, projectTrusted = getCursorSessionProjectTrusted()) {
|
|
24
|
+
return resolveEffectiveCursorConfig({ cwd, projectTrusted });
|
|
25
25
|
}
|
|
26
|
-
function buildLocalCursorProviderTurnLifecycle(lease, scopeKey) {
|
|
26
|
+
function buildLocalCursorProviderTurnLifecycle(lease, scopeKey, disposeAgentAfterTurn) {
|
|
27
27
|
return {
|
|
28
28
|
trackRunCompletion: (completion) => lease.trackRunCompletion(completion),
|
|
29
29
|
commitSend: (context, bootstrapped) => lease.commitSend(context, bootstrapped),
|
|
30
30
|
abandon: () => abandonSessionCursorAgent(scopeKey),
|
|
31
|
-
dispose: async () => {
|
|
31
|
+
dispose: async () => {
|
|
32
|
+
if (disposeAgentAfterTurn)
|
|
33
|
+
await resetSessionCursorAgent(scopeKey);
|
|
34
|
+
},
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
37
|
async function prepareCursorLocalProviderTurn(prepareParams) {
|
|
35
38
|
const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled } = prepareParams;
|
|
36
|
-
const { model, context, options } = params;
|
|
39
|
+
const { model, context, options, runtimeContext } = params;
|
|
37
40
|
let restoreCursorSdkOutputFilter;
|
|
38
41
|
let sessionAgentScopeKey;
|
|
39
42
|
let liveRun;
|
|
@@ -54,6 +57,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
|
|
|
54
57
|
const queuedBridgeRequestsBeforeLiveRun = [];
|
|
55
58
|
let liveRunForBridgeQueue;
|
|
56
59
|
const bridgeExcludeToolNames = buildCursorBridgeExcludeToolNames(resolvedConfig);
|
|
60
|
+
const localResumeEnabled = runtimeContext?.localResume ?? resolvedConfig.local.resume.value;
|
|
57
61
|
const sessionAgentAcquireParams = {
|
|
58
62
|
apiKey: resolvedApiKey,
|
|
59
63
|
agentMode,
|
|
@@ -61,8 +65,12 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
|
|
|
61
65
|
modelSelection: selection,
|
|
62
66
|
settingSources,
|
|
63
67
|
localSafety,
|
|
64
|
-
localResume:
|
|
68
|
+
localResume: localResumeEnabled,
|
|
65
69
|
useHttp1ForAgent,
|
|
70
|
+
runtimeScope: runtimeContext
|
|
71
|
+
? { scopeKey: runtimeContext.scopeKey, sessionFile: runtimeContext.sessionFile }
|
|
72
|
+
: undefined,
|
|
73
|
+
bridge: runtimeContext?.bridge,
|
|
66
74
|
bridgeExcludeToolNames,
|
|
67
75
|
debugRecorder: sdkEventDebug,
|
|
68
76
|
onBridgeToolRequest: (request) => {
|
|
@@ -124,7 +132,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
|
|
|
124
132
|
};
|
|
125
133
|
const sessionBridgeRun = bridgeRun;
|
|
126
134
|
const promptInputTokens = estimateCursorPromptTokens(prompt, promptOptions);
|
|
127
|
-
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
135
|
+
const useNativeToolReplay = runtimeContext?.nativeToolReplay ?? isCursorNativeToolDisplayRuntimeEnabled();
|
|
128
136
|
const activeToolNames = getActiveContextToolNames(context);
|
|
129
137
|
sdkEventDebug?.recordProviderMeta({
|
|
130
138
|
model: {
|
|
@@ -142,7 +150,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
|
|
|
142
150
|
toolManifestEnabled: resolveCursorToolManifestEnabled(),
|
|
143
151
|
agentMode,
|
|
144
152
|
localForce: resolvedConfig.local.force.value,
|
|
145
|
-
localResume:
|
|
153
|
+
localResume: localResumeEnabled,
|
|
146
154
|
resumedAgent: sessionAgentLease.resumed,
|
|
147
155
|
activeToolNames: activeToolNames ? [...activeToolNames] : [],
|
|
148
156
|
sessionAgentScopeKey,
|
|
@@ -204,7 +212,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
|
|
|
204
212
|
sessionAgentLease,
|
|
205
213
|
localForce: resolvedConfig.local.force,
|
|
206
214
|
restoreCursorSdkOutputFilter,
|
|
207
|
-
lifecycle: buildLocalCursorProviderTurnLifecycle(sessionAgentLease, sessionAgentScopeKey),
|
|
215
|
+
lifecycle: buildLocalCursorProviderTurnLifecycle(sessionAgentLease, sessionAgentScopeKey, runtimeContext?.disposeAgentAfterTurn === true),
|
|
208
216
|
runtime: liveRun
|
|
209
217
|
? { kind: "live", liveRun, turnCoordinator }
|
|
210
218
|
: { kind: "direct", turnCoordinator },
|
|
@@ -42,7 +42,8 @@ export class CursorProviderTurnRunner {
|
|
|
42
42
|
});
|
|
43
43
|
try {
|
|
44
44
|
this.throwIfAborted();
|
|
45
|
-
const
|
|
45
|
+
const runtimeContext = this.params.runtimeContext;
|
|
46
|
+
const cwd = runtimeContext?.cwd ?? getCursorSessionCwd();
|
|
46
47
|
this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
|
|
47
48
|
cwd,
|
|
48
49
|
modelId: model.id,
|
|
@@ -50,10 +51,10 @@ export class CursorProviderTurnRunner {
|
|
|
50
51
|
});
|
|
51
52
|
sdkEventDebugRef.current = this.sdkEventDebug;
|
|
52
53
|
this.sdkEventDebug?.recordContextSnapshot(context);
|
|
53
|
-
const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
|
|
54
|
-
const localScopeKey = getCursorSessionScopeKey();
|
|
54
|
+
const resolvedConfig = resolveCursorProviderTurnConfig(cwd, runtimeContext?.projectTrusted);
|
|
55
|
+
const localScopeKey = runtimeContext?.scopeKey ?? getCursorSessionScopeKey();
|
|
55
56
|
sdkProcessErrorGuard.containLocalTransportClosedPipe(() => invalidateSessionAgent(localScopeKey, { deadTransport: true }));
|
|
56
|
-
if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug)) ===
|
|
57
|
+
if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug, localScopeKey)) ===
|
|
57
58
|
"stream_ended") {
|
|
58
59
|
return;
|
|
59
60
|
}
|
package/dist/cursor-provider.js
CHANGED
|
@@ -27,7 +27,7 @@ function makeInitialMessage(model) {
|
|
|
27
27
|
timestamp: Date.now(),
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
export function streamCursor(model, context, options) {
|
|
30
|
+
export function streamCursor(model, context, options, runtimeContext) {
|
|
31
31
|
const stream = createAssistantMessageEventStream();
|
|
32
32
|
const sdkEventDebugRef = {};
|
|
33
33
|
attachCursorSdkEventDebugPiStreamTap(stream, sdkEventDebugRef);
|
|
@@ -39,11 +39,12 @@ export function streamCursor(model, context, options) {
|
|
|
39
39
|
stream,
|
|
40
40
|
partial,
|
|
41
41
|
options,
|
|
42
|
+
runtimeContext,
|
|
42
43
|
sdkEventDebugRef,
|
|
43
44
|
});
|
|
44
45
|
try {
|
|
45
46
|
stream.push({ type: "start", partial });
|
|
46
|
-
await runExclusiveCursorSessionTurn(getCursorSessionScopeKey(), () => runner.run(installCursorSdkProcessErrorGuard()), options?.signal);
|
|
47
|
+
await runExclusiveCursorSessionTurn(runtimeContext?.scopeKey ?? getCursorSessionScopeKey(), () => runner.run(installCursorSdkProcessErrorGuard()), options?.signal);
|
|
47
48
|
}
|
|
48
49
|
catch (error) {
|
|
49
50
|
await runner.handleOuterCatch(error);
|
|
@@ -283,16 +283,18 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
283
283
|
let bridgeRun;
|
|
284
284
|
let sessionStore;
|
|
285
285
|
try {
|
|
286
|
-
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
286
|
+
const registeredBridge = params.bridge ?? getRegisteredCursorPiToolBridge();
|
|
287
287
|
if (registeredBridge) {
|
|
288
|
-
|
|
288
|
+
const createdBridgeRun = await registeredBridge.createRun({
|
|
289
289
|
onToolRequest: params.onBridgeToolRequest,
|
|
290
290
|
debugRecorder: params.debugRecorder,
|
|
291
291
|
excludeToolNames: params.bridgeExcludeToolNames,
|
|
292
292
|
});
|
|
293
|
-
if (!
|
|
294
|
-
await
|
|
295
|
-
|
|
293
|
+
if (!createdBridgeRun.enabled || !createdBridgeRun.mcpServers) {
|
|
294
|
+
await createdBridgeRun.dispose();
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
bridgeRun = createdBridgeRun;
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
const resolvedPoolKey = buildSessionAgentPoolKey(scopeKey, params);
|
|
@@ -382,8 +384,8 @@ export function invalidateSessionAgent(scopeKey = getCursorSessionScopeKey(), op
|
|
|
382
384
|
deadTransportScopeKeys.add(scopeKey);
|
|
383
385
|
}
|
|
384
386
|
export async function acquireSessionCursorAgent(params) {
|
|
385
|
-
const scopeKey = getCursorSessionScopeKey();
|
|
386
|
-
const persistentStore = getCursorSessionFile() !== undefined;
|
|
387
|
+
const scopeKey = params.runtimeScope?.scopeKey ?? getCursorSessionScopeKey();
|
|
388
|
+
const persistentStore = params.runtimeScope ? params.runtimeScope.sessionFile !== undefined : getCursorSessionFile() !== undefined;
|
|
387
389
|
let forceCreate = params.forceCreate === true;
|
|
388
390
|
while (true) {
|
|
389
391
|
assertScopeAcceptsAcquire(scopeKey);
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { discoverModels } from "./model-discovery.js";
|
|
2
3
|
import { registerCursorRuntimeControls } from "./cursor-state.js";
|
|
3
4
|
import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display-registration.js";
|
|
4
|
-
import { registerCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
|
|
5
|
+
import { registerCursorPiToolBridge, registerNestedCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
|
|
5
6
|
import { registerCursorQuestionTool } from "./cursor-question-tool.js";
|
|
6
7
|
import { registerCursorSkillTool } from "./cursor-skill-tool.js";
|
|
7
8
|
import { registerCursorSessionScope } from "./cursor-session-scope.js";
|
|
@@ -15,24 +16,61 @@ import { registerCursorAgentsContextDedup } from "./cursor-agents-context-regist
|
|
|
15
16
|
import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
|
|
16
17
|
import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
|
|
17
18
|
import { prepareCursorSessionForCompaction } from "./cursor-session-compaction-prep.js";
|
|
19
|
+
import { disposeSessionCursorAgent } from "./cursor-session-agent.js";
|
|
20
|
+
import { getCursorSessionCwd, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
|
|
18
21
|
import { claimCursorExtensionFactory, registerCursorExtensionFactoryRelease, releaseCursorExtensionFactory, } from "./cursor-extension-factory-guard.js";
|
|
19
|
-
|
|
22
|
+
let activeCursorProviderModels;
|
|
23
|
+
function createCursorProviderConfig(models, streamSimple = streamCursorLazy) {
|
|
20
24
|
return {
|
|
21
25
|
name: "Cursor",
|
|
22
26
|
baseUrl: "https://cursor.com",
|
|
23
27
|
apiKey: CURSOR_API_KEY_CONFIG_VALUE,
|
|
24
28
|
api: "cursor-sdk",
|
|
25
29
|
models,
|
|
26
|
-
streamSimple
|
|
30
|
+
streamSimple,
|
|
27
31
|
};
|
|
28
32
|
}
|
|
29
|
-
function registerCursorProvider(pi, models) {
|
|
30
|
-
pi.registerProvider("cursor", createCursorProviderConfig(models));
|
|
33
|
+
function registerCursorProvider(pi, models, streamSimple) {
|
|
34
|
+
pi.registerProvider("cursor", createCursorProviderConfig(models, streamSimple));
|
|
35
|
+
}
|
|
36
|
+
function registerNestedCursorProvider(pi, models) {
|
|
37
|
+
const bridge = registerNestedCursorPiToolBridge(pi);
|
|
38
|
+
const nestedRuntimeId = randomUUID();
|
|
39
|
+
let runtimeContext = {
|
|
40
|
+
scopeKey: `__nested_cursor__:${nestedRuntimeId}`,
|
|
41
|
+
cwd: getCursorSessionCwd(),
|
|
42
|
+
sessionFile: undefined,
|
|
43
|
+
projectTrusted: getCursorSessionProjectTrusted(),
|
|
44
|
+
bridge,
|
|
45
|
+
localResume: false,
|
|
46
|
+
nativeToolReplay: false,
|
|
47
|
+
disposeAgentAfterTurn: true,
|
|
48
|
+
};
|
|
49
|
+
pi.on("session_start", (_event, ctx) => {
|
|
50
|
+
const sessionFile = ctx.sessionManager?.getSessionFile?.() ?? undefined;
|
|
51
|
+
const sessionId = ctx.sessionManager?.getSessionId?.() ?? nestedRuntimeId;
|
|
52
|
+
runtimeContext = {
|
|
53
|
+
...runtimeContext,
|
|
54
|
+
scopeKey: sessionFile ?? `__nested_cursor__:${sessionId}`,
|
|
55
|
+
cwd: ctx.cwd,
|
|
56
|
+
sessionFile,
|
|
57
|
+
projectTrusted: ctx.isProjectTrusted?.() === true || runtimeContext.projectTrusted,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
pi.on("session_shutdown", async () => {
|
|
61
|
+
await disposeSessionCursorAgent(runtimeContext.scopeKey);
|
|
62
|
+
});
|
|
63
|
+
registerCursorProvider(pi, models, (model, context, options) => streamCursorLazy(model, context, options, runtimeContext));
|
|
31
64
|
}
|
|
32
65
|
export default async function (pi) {
|
|
33
66
|
const factoryClaim = claimCursorExtensionFactory();
|
|
34
|
-
if (factoryClaim.kind === "nested")
|
|
67
|
+
if (factoryClaim.kind === "nested") {
|
|
68
|
+
if (!activeCursorProviderModels) {
|
|
69
|
+
throw new Error("Nested Cursor provider loaded before the owner model catalog was ready");
|
|
70
|
+
}
|
|
71
|
+
registerNestedCursorProvider(pi, activeCursorProviderModels);
|
|
35
72
|
return;
|
|
73
|
+
}
|
|
36
74
|
try {
|
|
37
75
|
// Discover first. A discovery failure must not leave process-global
|
|
38
76
|
// registrars from a discarded extension load.
|
|
@@ -42,6 +80,7 @@ export default async function (pi) {
|
|
|
42
80
|
fallbackIssue = issue;
|
|
43
81
|
},
|
|
44
82
|
});
|
|
83
|
+
activeCursorProviderModels = models;
|
|
45
84
|
// Session cwd must register before other session_start listeners that depend on it.
|
|
46
85
|
registerCursorSessionScope(pi);
|
|
47
86
|
registerCursorSessionAgentLineage(pi);
|
|
@@ -29,7 +29,7 @@ Current implementation notes:
|
|
|
29
29
|
- The bridge queues MCP calls, emits provider `toolcall_*` events, waits for matching pi `toolResult` messages by `toolCallId`, resolves the result back into the same live Cursor SDK run without creating a new `Agent`, and never calls tool `execute()` handlers directly. The same-run resume invariant holds unless the run was disposed, aborted, or cancelled.
|
|
30
30
|
- Cursor SDK MCP tool calls use a guarded timeout override because installed `@cursor/sdk` 1.0.30 still has a 60-second MCP request default with no public per-server timeout option. The extension extends the verified Cursor SDK MCP `callTool` timeout path to 3600 seconds by default and shortens the verified first-send MCP initialize/listTools timeout paths to 10 seconds by default so unavailable configured MCP servers do not block the first reply for a full minute; unknown MCP protocol timeout stacks keep the SDK default. Users can override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`. Bridged `CallTool` waits also have a local fail-closed deadline that defaults to and cannot exceed the effective MCP tool timeout; `PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS` can lower it, expiry or MCP cancellation aborts active pi execution when available, and expired bridge events are dropped before pi tool emission.
|
|
31
31
|
- Cursor SDK local safety controls are off by default. `--cursor-auto-review` / `PI_CURSOR_AUTO_REVIEW` and `--cursor-sandbox` / `PI_CURSOR_SANDBOX` pass only explicit enabled values into `Agent.create({ local })`; user or trusted project config can set `local.autoReview` and `local.sandboxOptions.enabled`; project config is active only when Pi's project-trust flow reached the extension and approved the project or the run used explicit `--approve`, and project saves require the same immutable trust provenance rather than creating Pi trust resources automatically. Pi 0.84.0 loads `pi install -l` project-local extensions after the trust event, so those installs require `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. Fast-default and HTTP transport saves preserve unrecognized config fields, reject malformed or non-object JSON without rewriting it, and use one lock-protected read-modify-write path; fast saves mutate only the selected model key. Because Pi can mutate its in-memory session branch before a journal append throws, a completed global save is authoritative and the command reports the partial journal failure instead of attempting an ambiguous rollback; the new global value stays authoritative over stale branch entries until a later successful save or session restart.
|
|
32
|
-
- Local HTTP/1.1/SSE compatibility is strictly opt-in through `PI_CURSOR_HTTP_1_1`, `/cursor-http on|off|toggle`, or user `cursor-sdk.json` `local.useHttp1ForAgent`. Precedence is session, environment, user, then the built-in unset default; project config is excluded. Unset makes no `Cursor.configure()` call. Explicit values configure the installed SDK before local `Agent.create()`, extension-owned explicit state is cleared with the SDK's documented `null` reset when returning to unset and during session shutdown before module reload, and default/HTTP2/HTTP1 choices split pooled local agents. Pi's supported CLI/TUI/print/RPC lifecycle has one active session runtime per process; concurrent independent `AgentSession` embedding in one process is outside this transport toggle's contract because the installed SDK setting and executor cache are module-global. The extension factory
|
|
32
|
+
- Local HTTP/1.1/SSE compatibility is strictly opt-in through `PI_CURSOR_HTTP_1_1`, `/cursor-http on|off|toggle`, or user `cursor-sdk.json` `local.useHttp1ForAgent`. Precedence is session, environment, user, then the built-in unset default; project config is excluded. Unset makes no `Cursor.configure()` call. Explicit values configure the installed SDK before local `Agent.create()`, extension-owned explicit state is cleared with the SDK's documented `null` reset when returning to unset and during session shutdown before module reload, and default/HTTP2/HTTP1 choices split pooled local agents. Pi's supported CLI/TUI/print/RPC lifecycle has one active session runtime per process; concurrent independent `AgentSession` embedding in one process is outside this transport toggle's contract because the installed SDK setting and executor cache are module-global. The extension factory has process-owner and nested-child paths. The first load owns process-global controls, session scope, native replay state, and the owner SDK agent pool. A nested child `createAgentSession` load registers only a child-local Cursor provider, bridge, and SDK agent scope. This split keeps the parent bridge live and lets a Cursor child run without waiting on the busy parent SDK agent. The footer adds `http1` only when HTTP/1.1 transport is enabled.
|
|
33
33
|
- Bridge diagnostics are opt-in only: `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` writes typed, allowlisted, scrubbed single-line JSONL records to `process.stderr` with prefix `[pi-cursor-sdk:bridge]`. Diagnostics are scrubbed operational logs, not anonymous telemetry. They intentionally include tool names, safe correlation IDs, run lifecycle, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. Correlation IDs are generated independently from the tokenized endpoint path, and Cursor MCP call IDs are hashed before serialization. Diagnostics must not include endpoint paths/URLs/path components/tokens, API keys, bearer tokens, cookies, session credentials, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, or local private session paths in tracked docs, and they must not call pi UI status, notification, or footer APIs. If tool names themselves are unacceptable for a release target, bridge debug diagnostics are not safe for shared logs under the current contract.
|
|
34
34
|
- This repo does not provide a generic desktop-automation, browser-driver, or CDP recipe. Provider docs should describe pi-cursor-sdk's Cursor provider/bridge contract only.
|
|
35
35
|
- Cursor internal tool activity is recorded from SDK events and scrubbed. Maintainer reference for `@cursor/sdk@1.0.30` `ToolType` values, runtime alias normalization, and intentional mapping/fallback rules: [Cursor native tool replay — SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix) (official SDK docs: https://cursor.com/docs/sdk/typescript). In TUI sessions and structured JSON/RPC modes, supported completed `read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`, diagnostics, delete, todo/plan, task, image generation, MCP, semantic search, and screen recording activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so users and JSON/RPC consumers can see native-looking cards/events without rerunning Cursor's reads/shell commands/file edits. Cursor `glob` activity is replayed through native `find` cards. Cursor write activity is replayed through native-looking `write` cards, and Cursor StrReplace/edit activity uses native-looking `edit` only when recorded arguments truthfully satisfy pi's `edit` schema; path-only Cursor edit and notebook edit replay falls back to neutral Cursor activity before pi validation. Diagnostics, delete, todos/plans, task/subagent, image, and MCP activity use neutral Cursor activity cards with pi's default success/error shell. Cursor SDK `task` activity is labeled **Cursor subagent** by default because it represents Cursor-spawned child-agent work; the card summary includes description plus subagent kind/model/short ID when Cursor reports them, and `PI_CURSOR_TASK_PRESENTATION=task` restores the older **Cursor task** wording for comparison. This is visibility over Cursor SDK task events, not a native pi subagent session: pi shows start/final output plus any `conversationSteps` tool-call summaries Cursor returns, but cannot show a live nested read/shell/MCP trail when the SDK only returns final subagent text. Neutral Cursor activity calls include `activityTitle` and, when available, `activitySummary` so partial/collapsed cards preserve identity such as `Cursor plan`, `Cursor todos`, `Cursor subagent`, `Cursor MCP`, or `Cursor edit`. For long-running or externally meaningful Cursor tools (`task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch, plan/todo), the provider may surface one low-noise deferred in-progress thinking line such as `Cursor MCP: external_search` from bounded, scrubbed SDK args; fast local tools (`read`, `grep`, `glob`, and similar) skip lifecycle lines when completion follows immediately, and pi bridge MCP calls are excluded because pi already shows real pi tool execution ([lifecycle visibility](./cursor-native-tool-replay.md#low-noise-tool-lifecycle-visibility)). Replay-only tools display recorded Cursor results, normalize workspace-local paths/diff headers for display, use pi diff colors for edit previews and path-inferred syntax highlighting for write previews, and fail closed if called without a recorded result. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. Cursor workflow tools such as mode/task/todo/plan activity are not pi workflow controls; reported todo/plan events are displayed as Cursor activity only. Plan/todo replay cards can be followed by Cursor's final plan text, selected from `run.wait().result` when Cursor provides one and trimmed against already-emitted text. Started Cursor SDK tool calls that never receive a completion event are surfaced with bounded user-visible labels/traces (neutral activity cards when native replay routing allows, otherwise the same inactive or transcript trace fallbacks used for completed replay) instead of being silently discarded when the run failed, was aborted, or produced no assistant text; after a successful text-producing run, missing-completion starts remain maintainer-debug-only for all tools: installed `@cursor/sdk` 1.0.30 emits `tool-call-started` with no completion delta, step, or conversation entry when a permission policy or hook denies a call, and offers no way to distinguish such denials from lost completions, so suppression is the deliberate choice over false error cards. Explicit failures remain visible when Cursor reports them through completed tool calls or step results. Pi bridge MCP starts remain excluded from duplicate incomplete Cursor cards because pi already shows real pi tool execution. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When bridge or native replay cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK activity arrives: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later tool batches as further `toolUse` turns, then Cursor's final assistant answer. For shell replay, completed `stdout` / `stderr` are primary; unambiguous `shell-output-delta` data is also shown as bounded live progress while one shell call is active and used as display-only fallback for empty successful shell completions, while overlapping shell calls drop ambiguous deltas instead of guessing. Print mode keeps bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when no live-run turn split is active.
|
|
@@ -32,16 +32,25 @@ When changing provider/runtime behavior, ask whether the bug spans **pi extensio
|
|
|
32
32
|
|
|
33
33
|
Pi subagents build a child `AgentSession` in the same process (`createAgentSession` + `bindExtensions`). That re-invokes the `pi-cursor-sdk` factory against a new ExtensionAPI while the parent Cursor run still owns the process-global bridge, session scope, and pooled SDK agent.
|
|
34
34
|
|
|
35
|
-
The observed failure: the child
|
|
35
|
+
The first observed failure: the child died at `0 tool uses` with no transcript file. The parent turn aborted with `This operation was aborted`. Bridge diagnostics showed `request_rejected` / `cancelled` with `Cursor pi tool bridge extension reloaded`.
|
|
36
|
+
|
|
37
|
+
The process-owner guard fixed that abort, but an unconditional nested-factory return introduced a second failure. A child that selected `cursor/grok-4.6` had model metadata from the parent but no `cursor` provider in its own model runtime. Pi reported `unrecognized provider error` before the first child turn.
|
|
38
|
+
|
|
39
|
+
Nested factories must split registration by ownership:
|
|
40
|
+
|
|
41
|
+
- The owner registers process-global controls, session state, native replay, and the owner bridge.
|
|
42
|
+
- Each nested child registers the Cursor provider on its own ExtensionAPI.
|
|
43
|
+
- Each nested child uses its own scope key and SDK agent pool entry. A Cursor child must not wait for the busy parent agent that is waiting for the subagent result.
|
|
44
|
+
- Each nested child uses its own bridge registry over the child's active pi tools. Child shutdown must not call the process-wide bridge abort path.
|
|
45
|
+
- Nested children disable local resume and native replay wrappers. They dispose their isolated SDK agent after the provider run completes.
|
|
36
46
|
|
|
37
47
|
Regression coverage:
|
|
38
48
|
|
|
39
|
-
- `src/cursor-extension-factory-guard.ts` — the first factory owns the process; nested loads no-op until the owner session shuts down and Pi can create its replacement runtime
|
|
40
49
|
- `test/cursor-extension-factory-guard.test.ts` — owner tokens reject stale release and release for every Pi shutdown reason
|
|
41
|
-
- `test/index-factory-guard.test.ts` — nested
|
|
42
|
-
- `test/cursor-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
- `test/index-factory-guard.test.ts` — nested factories register only the child-local provider and bridge hooks without stealing owner scope
|
|
51
|
+
- `test/cursor-session-agent.test.ts` — a nested scope acquires its own SDK agent while the parent scope is busy
|
|
52
|
+
- `test/cursor-pi-tool-bridge.test.ts` — nested bridge shutdown does not reject a pending owner bridge call
|
|
53
|
+
- `test/cursor-provider-run-finalizer.test.ts` — live provider completion runs the nested lifecycle disposal path
|
|
45
54
|
|
|
46
55
|
## Dual-check invariant: `context.tools` vs pi active tools
|
|
47
56
|
|
package/package.json
CHANGED
|
@@ -111,6 +111,11 @@ function isPendingBridgeToolRequest(run: CursorLiveRun, request: CursorPiBridgeT
|
|
|
111
111
|
return bridgeRun?.hasPendingPiToolCallId(request.piToolCallId) === true;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function hasPendingBridgeCalls(run: CursorLiveRun): boolean {
|
|
115
|
+
if (run.bridgeRun?.hasPendingCalls()) return true;
|
|
116
|
+
return run.sessionBridgeRun !== run.bridgeRun && run.sessionBridgeRun?.hasPendingCalls() === true;
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
export interface CursorLiveRunRecord {
|
|
115
120
|
id: string;
|
|
116
121
|
disposed: boolean;
|
|
@@ -494,6 +499,12 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
494
499
|
if (state.leased || state.leaseQueue.length > 0) return;
|
|
495
500
|
state.idleDisposeRequested = false;
|
|
496
501
|
state.idleDisposeTimer = setTimeout(() => {
|
|
502
|
+
if (hasPendingBridgeCalls(run)) {
|
|
503
|
+
// Bridge calls have their own deadline. Reuse the idle interval to avoid a hot
|
|
504
|
+
// polling loop if the SDK does not emit a terminal event after that deadline.
|
|
505
|
+
coordinator.requestIdleDispose(run);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
497
508
|
void coordinator.release(run).catch(() => {
|
|
498
509
|
// Idle dispose must not leave release failures as unhandled rejections.
|
|
499
510
|
});
|
|
@@ -183,6 +183,10 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
183
183
|
await this.resolveToolResults(context.messages.map(asToolResultMessage).filter((message): message is ToolResultMessage => message !== undefined));
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
hasPendingCalls(): boolean {
|
|
187
|
+
return this.pendingCount() > 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
186
190
|
hasPendingPiToolCallId(piToolCallId: string): boolean {
|
|
187
191
|
return this.pendingByPiToolCallId.has(piToolCallId);
|
|
188
192
|
}
|
|
@@ -65,6 +65,7 @@ export interface CursorPiToolBridgeRun {
|
|
|
65
65
|
takeQueuedToolRequests(): CursorPiBridgeToolRequest[];
|
|
66
66
|
resolveToolResults(toolResults: readonly ToolResultMessage[]): Promise<void>;
|
|
67
67
|
resolveToolResultsFromContext(context: Context): Promise<void>;
|
|
68
|
+
hasPendingCalls(): boolean;
|
|
68
69
|
hasPendingPiToolCallId(piToolCallId: string): boolean;
|
|
69
70
|
isBridgeMcpToolCall(toolCall: unknown): boolean;
|
|
70
71
|
setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void;
|
|
@@ -88,18 +88,13 @@ Get-CimInstance Win32_Process -Filter "Name = 'bash.exe' OR Name = 'sh.exe'" |
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
|
|
98
|
-
void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
|
|
99
|
-
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
100
|
-
registeredCursorPiToolBridge = bridge;
|
|
91
|
+
function attachCursorPiToolBridgeHandlers(
|
|
92
|
+
pi: CursorPiToolBridgeExtensionApi,
|
|
93
|
+
bridge: CursorPiToolBridgeRegistry,
|
|
94
|
+
options: { abortAllOnShutdown: boolean; isActive: () => boolean },
|
|
95
|
+
): void {
|
|
101
96
|
pi.on("tool_call", (event, ctx) => {
|
|
102
|
-
if (
|
|
97
|
+
if (!options.isActive()) return undefined;
|
|
103
98
|
if (!bridge.hasPendingPiToolCallId(event.toolCallId)) {
|
|
104
99
|
return isCursorPiBridgeToolCallId(event.toolCallId)
|
|
105
100
|
? { block: true, reason: "Cursor pi bridge tool call is no longer pending" }
|
|
@@ -124,9 +119,35 @@ export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi):
|
|
|
124
119
|
});
|
|
125
120
|
pi.on("session_shutdown", async (event) => {
|
|
126
121
|
const reason = `Cursor pi tool bridge session shutdown: ${event.reason}`;
|
|
127
|
-
bridgeToolExecutionAbortTracker.abortAll(reason);
|
|
122
|
+
if (options.abortAllOnShutdown) bridgeToolExecutionAbortTracker.abortAll(reason);
|
|
128
123
|
await bridge.disposeAll(reason);
|
|
129
124
|
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi): CursorPiToolBridge {
|
|
128
|
+
// Replacing a bridge during a live MCP run cancels its pending pi tool
|
|
129
|
+
// calls. Keep the active registry as a final safety belt.
|
|
130
|
+
if (registeredCursorPiToolBridge?.hasLiveRuns()) {
|
|
131
|
+
return registeredCursorPiToolBridge;
|
|
132
|
+
}
|
|
133
|
+
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
|
|
134
|
+
void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
|
|
135
|
+
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
136
|
+
registeredCursorPiToolBridge = bridge;
|
|
137
|
+
attachCursorPiToolBridgeHandlers(pi, bridge, {
|
|
138
|
+
abortAllOnShutdown: true,
|
|
139
|
+
isActive: () => registeredCursorPiToolBridge === bridge,
|
|
140
|
+
});
|
|
141
|
+
return bridge;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Register a bridge owned only by one nested in-process child session. */
|
|
145
|
+
export function registerNestedCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi): CursorPiToolBridge {
|
|
146
|
+
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
147
|
+
attachCursorPiToolBridgeHandlers(pi, bridge, {
|
|
148
|
+
abortAllOnShutdown: false,
|
|
149
|
+
isActive: () => true,
|
|
150
|
+
});
|
|
130
151
|
return bridge;
|
|
131
152
|
}
|
|
132
153
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
10
|
import { streamCursor } from "./cursor-provider.js";
|
|
11
11
|
import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
|
|
12
|
+
import type { CursorProviderRuntimeContext } from "./cursor-provider-runtime-context.js";
|
|
12
13
|
|
|
13
14
|
function makeProviderRuntimeErrorMessage(model: Model<Api>, error: unknown, apiKey?: string): AssistantMessage {
|
|
14
15
|
return {
|
|
@@ -35,11 +36,12 @@ export function streamCursorLazy(
|
|
|
35
36
|
model: Model<Api>,
|
|
36
37
|
context: Context,
|
|
37
38
|
options?: SimpleStreamOptions,
|
|
39
|
+
runtimeContext?: CursorProviderRuntimeContext,
|
|
38
40
|
): AssistantMessageEventStream {
|
|
39
41
|
const outer = createAssistantMessageEventStream();
|
|
40
42
|
queueMicrotask(async () => {
|
|
41
43
|
try {
|
|
42
|
-
for await (const event of streamCursor(model, context, options)) {
|
|
44
|
+
for await (const event of streamCursor(model, context, options, runtimeContext)) {
|
|
43
45
|
outer.push(event);
|
|
44
46
|
}
|
|
45
47
|
} catch (error) {
|
|
@@ -67,8 +67,8 @@ export function getPendingCursorLiveRun(context: Context): CursorLiveRun | undef
|
|
|
67
67
|
return cursorLiveRuns.getPendingFromContext(context, getCursorNativeReplayIdFromToolCallId);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export function
|
|
71
|
-
return cursorLiveRuns.getActiveForScope();
|
|
70
|
+
export function getActiveCursorLiveRunForScope(scopeKey?: string): CursorLiveRun | undefined {
|
|
71
|
+
return cursorLiveRuns.getActiveForScope(scopeKey);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function splitTextIntoReplayDeltas(text: string): string[] {
|
|
@@ -427,10 +427,11 @@ export async function drainExistingCursorLiveRunBeforeSend(
|
|
|
427
427
|
context: Context,
|
|
428
428
|
signal?: AbortSignal,
|
|
429
429
|
turnDebugRecorder?: CursorSdkEventDebugRecorder,
|
|
430
|
+
scopeKey?: string,
|
|
430
431
|
): Promise<LiveRunPreSendOutcome> {
|
|
431
432
|
turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
|
|
432
433
|
while (true) {
|
|
433
|
-
const run = getPendingCursorLiveRun(context) ??
|
|
434
|
+
const run = getPendingCursorLiveRun(context) ?? getActiveCursorLiveRunForScope(scopeKey);
|
|
434
435
|
if (!run || run.disposed) {
|
|
435
436
|
turnDebugRecorder?.recordDrainEvent("pre_send_end", { outcome: "continue_send", reason: "no_pending_run" });
|
|
436
437
|
return "continue_send";
|
|
@@ -147,6 +147,7 @@ export class CursorRunFinalizer {
|
|
|
147
147
|
if (liveCompletion) {
|
|
148
148
|
void liveCompletion.waitCompletion
|
|
149
149
|
.finally(async () => {
|
|
150
|
+
await prepared?.lifecycle.dispose().catch(() => {});
|
|
150
151
|
await this.finalizeSdkEventDebugBestEffort();
|
|
151
152
|
this.safeCleanup(() => this.params.sdkProcessErrorGuard.dispose());
|
|
152
153
|
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CursorPiToolBridge } from "./cursor-pi-tool-bridge-types.js";
|
|
2
|
+
|
|
3
|
+
/** Runtime state captured by one nested in-process Cursor provider registration. */
|
|
4
|
+
export interface CursorProviderRuntimeContext {
|
|
5
|
+
readonly scopeKey: string;
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly sessionFile: string | undefined;
|
|
8
|
+
readonly projectTrusted: boolean;
|
|
9
|
+
readonly bridge: CursorPiToolBridge;
|
|
10
|
+
readonly localResume: false;
|
|
11
|
+
readonly nativeToolReplay: false;
|
|
12
|
+
readonly disposeAgentAfterTurn: true;
|
|
13
|
+
}
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
abandonSessionCursorAgent,
|
|
21
21
|
createCursorNativeReplayId,
|
|
22
22
|
cursorLiveRuns,
|
|
23
|
-
getActiveCursorLiveRunForCurrentScope,
|
|
24
23
|
getPendingCursorLiveRun,
|
|
25
24
|
} from "./cursor-provider-live-run-drain.js";
|
|
26
25
|
import {
|
|
@@ -66,19 +65,22 @@ interface PrepareCursorProviderTurnContext extends PrepareCursorProviderTurnPara
|
|
|
66
65
|
fastEnabled: boolean | undefined;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
export function resolveCursorProviderTurnConfig(cwd: string) {
|
|
70
|
-
return resolveEffectiveCursorConfig({ cwd, projectTrusted
|
|
68
|
+
export function resolveCursorProviderTurnConfig(cwd: string, projectTrusted = getCursorSessionProjectTrusted()) {
|
|
69
|
+
return resolveEffectiveCursorConfig({ cwd, projectTrusted });
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
function buildLocalCursorProviderTurnLifecycle(
|
|
74
73
|
lease: SessionCursorAgentLease,
|
|
75
74
|
scopeKey: string,
|
|
75
|
+
disposeAgentAfterTurn: boolean,
|
|
76
76
|
): CursorProviderTurnLifecycle {
|
|
77
77
|
return {
|
|
78
78
|
trackRunCompletion: (completion) => lease.trackRunCompletion(completion),
|
|
79
79
|
commitSend: (context, bootstrapped) => lease.commitSend(context, bootstrapped),
|
|
80
80
|
abandon: () => abandonSessionCursorAgent(scopeKey),
|
|
81
|
-
dispose: async () => {
|
|
81
|
+
dispose: async () => {
|
|
82
|
+
if (disposeAgentAfterTurn) await resetSessionCursorAgent(scopeKey);
|
|
83
|
+
},
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -86,7 +88,7 @@ async function prepareCursorLocalProviderTurn(
|
|
|
86
88
|
prepareParams: PrepareCursorProviderTurnContext,
|
|
87
89
|
): Promise<CursorProviderTurnPrepareResult> {
|
|
88
90
|
const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled } = prepareParams;
|
|
89
|
-
const { model, context, options } = params;
|
|
91
|
+
const { model, context, options, runtimeContext } = params;
|
|
90
92
|
|
|
91
93
|
let restoreCursorSdkOutputFilter: (() => void) | undefined;
|
|
92
94
|
let sessionAgentScopeKey: string | undefined;
|
|
@@ -113,6 +115,7 @@ async function prepareCursorLocalProviderTurn(
|
|
|
113
115
|
const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
|
|
114
116
|
let liveRunForBridgeQueue: CursorLiveRun | undefined;
|
|
115
117
|
const bridgeExcludeToolNames = buildCursorBridgeExcludeToolNames(resolvedConfig);
|
|
118
|
+
const localResumeEnabled = runtimeContext?.localResume ?? resolvedConfig.local.resume.value;
|
|
116
119
|
|
|
117
120
|
const sessionAgentAcquireParams = {
|
|
118
121
|
apiKey: resolvedApiKey,
|
|
@@ -121,8 +124,12 @@ async function prepareCursorLocalProviderTurn(
|
|
|
121
124
|
modelSelection: selection,
|
|
122
125
|
settingSources,
|
|
123
126
|
localSafety,
|
|
124
|
-
localResume:
|
|
127
|
+
localResume: localResumeEnabled,
|
|
125
128
|
useHttp1ForAgent,
|
|
129
|
+
runtimeScope: runtimeContext
|
|
130
|
+
? { scopeKey: runtimeContext.scopeKey, sessionFile: runtimeContext.sessionFile }
|
|
131
|
+
: undefined,
|
|
132
|
+
bridge: runtimeContext?.bridge,
|
|
126
133
|
bridgeExcludeToolNames,
|
|
127
134
|
debugRecorder: sdkEventDebug,
|
|
128
135
|
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
@@ -185,7 +192,7 @@ async function prepareCursorLocalProviderTurn(
|
|
|
185
192
|
};
|
|
186
193
|
const sessionBridgeRun = bridgeRun;
|
|
187
194
|
const promptInputTokens = estimateCursorPromptTokens(prompt, promptOptions);
|
|
188
|
-
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
195
|
+
const useNativeToolReplay = runtimeContext?.nativeToolReplay ?? isCursorNativeToolDisplayRuntimeEnabled();
|
|
189
196
|
const activeToolNames = getActiveContextToolNames(context);
|
|
190
197
|
sdkEventDebug?.recordProviderMeta({
|
|
191
198
|
model: {
|
|
@@ -203,7 +210,7 @@ async function prepareCursorLocalProviderTurn(
|
|
|
203
210
|
toolManifestEnabled: resolveCursorToolManifestEnabled(),
|
|
204
211
|
agentMode,
|
|
205
212
|
localForce: resolvedConfig.local.force.value,
|
|
206
|
-
localResume:
|
|
213
|
+
localResume: localResumeEnabled,
|
|
207
214
|
resumedAgent: sessionAgentLease.resumed,
|
|
208
215
|
activeToolNames: activeToolNames ? [...activeToolNames] : [],
|
|
209
216
|
sessionAgentScopeKey,
|
|
@@ -266,7 +273,11 @@ async function prepareCursorLocalProviderTurn(
|
|
|
266
273
|
sessionAgentLease,
|
|
267
274
|
localForce: resolvedConfig.local.force,
|
|
268
275
|
restoreCursorSdkOutputFilter,
|
|
269
|
-
lifecycle: buildLocalCursorProviderTurnLifecycle(
|
|
276
|
+
lifecycle: buildLocalCursorProviderTurnLifecycle(
|
|
277
|
+
sessionAgentLease,
|
|
278
|
+
sessionAgentScopeKey,
|
|
279
|
+
runtimeContext?.disposeAgentAfterTurn === true,
|
|
280
|
+
),
|
|
270
281
|
runtime: liveRun
|
|
271
282
|
? { kind: "live", liveRun, turnCoordinator }
|
|
272
283
|
: { kind: "direct", turnCoordinator },
|
|
@@ -62,7 +62,8 @@ export class CursorProviderTurnRunner {
|
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
64
|
this.throwIfAborted();
|
|
65
|
-
const
|
|
65
|
+
const runtimeContext = this.params.runtimeContext;
|
|
66
|
+
const cwd = runtimeContext?.cwd ?? getCursorSessionCwd();
|
|
66
67
|
this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
|
|
67
68
|
cwd,
|
|
68
69
|
modelId: model.id,
|
|
@@ -70,13 +71,21 @@ export class CursorProviderTurnRunner {
|
|
|
70
71
|
});
|
|
71
72
|
sdkEventDebugRef.current = this.sdkEventDebug;
|
|
72
73
|
this.sdkEventDebug?.recordContextSnapshot(context);
|
|
73
|
-
const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
|
|
74
|
-
const localScopeKey = getCursorSessionScopeKey();
|
|
74
|
+
const resolvedConfig = resolveCursorProviderTurnConfig(cwd, runtimeContext?.projectTrusted);
|
|
75
|
+
const localScopeKey = runtimeContext?.scopeKey ?? getCursorSessionScopeKey();
|
|
75
76
|
sdkProcessErrorGuard.containLocalTransportClosedPipe(() =>
|
|
76
77
|
invalidateSessionAgent(localScopeKey, { deadTransport: true }),
|
|
77
78
|
);
|
|
78
79
|
if (
|
|
79
|
-
(await drainExistingCursorLiveRunBeforeSend(
|
|
80
|
+
(await drainExistingCursorLiveRunBeforeSend(
|
|
81
|
+
stream,
|
|
82
|
+
partial,
|
|
83
|
+
model,
|
|
84
|
+
context,
|
|
85
|
+
options?.signal,
|
|
86
|
+
this.sdkEventDebug,
|
|
87
|
+
localScopeKey,
|
|
88
|
+
)) ===
|
|
80
89
|
"stream_ended"
|
|
81
90
|
) {
|
|
82
91
|
return;
|
|
@@ -15,6 +15,7 @@ import type { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinato
|
|
|
15
15
|
import type { CursorPrompt } from "./context.js";
|
|
16
16
|
import type { CursorResolvedSetting } from "./cursor-config.js";
|
|
17
17
|
import type { CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
|
|
18
|
+
import type { CursorProviderRuntimeContext } from "./cursor-provider-runtime-context.js";
|
|
18
19
|
|
|
19
20
|
export interface CursorProviderTurnRunnerParams {
|
|
20
21
|
model: Model<Api>;
|
|
@@ -22,6 +23,7 @@ export interface CursorProviderTurnRunnerParams {
|
|
|
22
23
|
stream: AssistantMessageEventStream;
|
|
23
24
|
partial: AssistantMessage;
|
|
24
25
|
options?: SimpleStreamOptions;
|
|
26
|
+
runtimeContext?: CursorProviderRuntimeContext;
|
|
25
27
|
sdkEventDebugRef: { current?: CursorSdkEventDebugSink };
|
|
26
28
|
}
|
|
27
29
|
|
package/src/cursor-provider.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { resolveCursorApiKey } from "./cursor-api-key.js";
|
|
|
23
23
|
import { CursorProviderTurnRunner } from "./cursor-provider-turn-runner.js";
|
|
24
24
|
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
25
25
|
import { runExclusiveCursorSessionTurn, __testUtils as cursorSessionTurnQueueTestUtils } from "./cursor-session-turn-queue.js";
|
|
26
|
+
import type { CursorProviderRuntimeContext } from "./cursor-provider-runtime-context.js";
|
|
26
27
|
|
|
27
28
|
function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
28
29
|
return {
|
|
@@ -48,6 +49,7 @@ export function streamCursor(
|
|
|
48
49
|
model: Model<Api>,
|
|
49
50
|
context: Context,
|
|
50
51
|
options?: SimpleStreamOptions,
|
|
52
|
+
runtimeContext?: CursorProviderRuntimeContext,
|
|
51
53
|
): AssistantMessageEventStream {
|
|
52
54
|
const stream = createAssistantMessageEventStream();
|
|
53
55
|
const sdkEventDebugRef: { current?: CursorSdkEventDebugSink } = {};
|
|
@@ -62,13 +64,14 @@ export function streamCursor(
|
|
|
62
64
|
stream,
|
|
63
65
|
partial,
|
|
64
66
|
options,
|
|
67
|
+
runtimeContext,
|
|
65
68
|
sdkEventDebugRef,
|
|
66
69
|
});
|
|
67
70
|
|
|
68
71
|
try {
|
|
69
72
|
stream.push({ type: "start", partial });
|
|
70
73
|
await runExclusiveCursorSessionTurn(
|
|
71
|
-
getCursorSessionScopeKey(),
|
|
74
|
+
runtimeContext?.scopeKey ?? getCursorSessionScopeKey(),
|
|
72
75
|
() => runner.run(installCursorSdkProcessErrorGuard()),
|
|
73
76
|
options?.signal,
|
|
74
77
|
);
|
|
@@ -4,6 +4,7 @@ import type { Context } from "@earendil-works/pi-ai";
|
|
|
4
4
|
import {
|
|
5
5
|
getRegisteredCursorPiToolBridge,
|
|
6
6
|
type CursorPiBridgeToolRequest,
|
|
7
|
+
type CursorPiToolBridge,
|
|
7
8
|
type CursorPiToolBridgeRun,
|
|
8
9
|
} from "./cursor-pi-tool-bridge.js";
|
|
9
10
|
import { computeCursorContextFingerprint } from "./context.js";
|
|
@@ -136,6 +137,8 @@ interface SessionCursorAgentCreateParams {
|
|
|
136
137
|
bridgeExcludeToolNames?: ReadonlySet<string>;
|
|
137
138
|
onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
138
139
|
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
140
|
+
runtimeScope?: { scopeKey: string; sessionFile: string | undefined };
|
|
141
|
+
bridge?: CursorPiToolBridge;
|
|
139
142
|
localResume?: boolean;
|
|
140
143
|
forceCreate?: boolean;
|
|
141
144
|
createAgent?: CursorSdkModule["Agent"]["create"];
|
|
@@ -453,16 +456,17 @@ async function createSessionAgentEntry(
|
|
|
453
456
|
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
454
457
|
let sessionStore: OpenCursorSessionStore | undefined;
|
|
455
458
|
try {
|
|
456
|
-
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
459
|
+
const registeredBridge = params.bridge ?? getRegisteredCursorPiToolBridge();
|
|
457
460
|
if (registeredBridge) {
|
|
458
|
-
|
|
461
|
+
const createdBridgeRun = await registeredBridge.createRun({
|
|
459
462
|
onToolRequest: params.onBridgeToolRequest,
|
|
460
463
|
debugRecorder: params.debugRecorder,
|
|
461
464
|
excludeToolNames: params.bridgeExcludeToolNames,
|
|
462
465
|
});
|
|
463
|
-
if (!
|
|
464
|
-
await
|
|
465
|
-
|
|
466
|
+
if (!createdBridgeRun.enabled || !createdBridgeRun.mcpServers) {
|
|
467
|
+
await createdBridgeRun.dispose();
|
|
468
|
+
} else {
|
|
469
|
+
bridgeRun = createdBridgeRun;
|
|
466
470
|
}
|
|
467
471
|
}
|
|
468
472
|
|
|
@@ -559,8 +563,8 @@ export function invalidateSessionAgent(
|
|
|
559
563
|
}
|
|
560
564
|
|
|
561
565
|
export async function acquireSessionCursorAgent(params: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
|
|
562
|
-
const scopeKey = getCursorSessionScopeKey();
|
|
563
|
-
const persistentStore = getCursorSessionFile() !== undefined;
|
|
566
|
+
const scopeKey = params.runtimeScope?.scopeKey ?? getCursorSessionScopeKey();
|
|
567
|
+
const persistentStore = params.runtimeScope ? params.runtimeScope.sessionFile !== undefined : getCursorSessionFile() !== undefined;
|
|
564
568
|
let forceCreate = params.forceCreate === true;
|
|
565
569
|
|
|
566
570
|
while (true) {
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { ExtensionAPI, ProviderConfig, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { discoverModels, type CursorModelFallbackIssue } from "./model-discovery.js";
|
|
3
4
|
import { registerCursorRuntimeControls } from "./cursor-state.js";
|
|
4
5
|
import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display-registration.js";
|
|
5
|
-
import { registerCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
|
|
6
|
+
import { registerCursorPiToolBridge, registerNestedCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
|
|
6
7
|
import { registerCursorQuestionTool } from "./cursor-question-tool.js";
|
|
7
8
|
import { registerCursorSkillTool } from "./cursor-skill-tool.js";
|
|
8
9
|
import { registerCursorSessionScope } from "./cursor-session-scope.js";
|
|
@@ -16,6 +17,9 @@ import { registerCursorAgentsContextDedup } from "./cursor-agents-context-regist
|
|
|
16
17
|
import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
|
|
17
18
|
import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
|
|
18
19
|
import { prepareCursorSessionForCompaction } from "./cursor-session-compaction-prep.js";
|
|
20
|
+
import { disposeSessionCursorAgent } from "./cursor-session-agent.js";
|
|
21
|
+
import { getCursorSessionCwd, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
|
|
22
|
+
import type { CursorProviderRuntimeContext } from "./cursor-provider-runtime-context.js";
|
|
19
23
|
import {
|
|
20
24
|
claimCursorExtensionFactory,
|
|
21
25
|
registerCursorExtensionFactoryRelease,
|
|
@@ -39,24 +43,70 @@ type CursorExtensionApi =
|
|
|
39
43
|
& Parameters<typeof registerCursorSdkSessionProcessErrorGuard>[0]
|
|
40
44
|
& Parameters<typeof registerCursorExtensionFactoryRelease>[0];
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
let activeCursorProviderModels: ProviderModelConfig[] | undefined;
|
|
47
|
+
|
|
48
|
+
function createCursorProviderConfig(
|
|
49
|
+
models: ProviderModelConfig[],
|
|
50
|
+
streamSimple: NonNullable<ProviderConfig["streamSimple"]> = streamCursorLazy,
|
|
51
|
+
): ProviderConfig {
|
|
43
52
|
return {
|
|
44
53
|
name: "Cursor",
|
|
45
54
|
baseUrl: "https://cursor.com",
|
|
46
55
|
apiKey: CURSOR_API_KEY_CONFIG_VALUE,
|
|
47
56
|
api: "cursor-sdk",
|
|
48
57
|
models,
|
|
49
|
-
streamSimple
|
|
58
|
+
streamSimple,
|
|
50
59
|
};
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
function registerCursorProvider(
|
|
54
|
-
pi
|
|
62
|
+
function registerCursorProvider(
|
|
63
|
+
pi: Pick<ExtensionAPI, "registerProvider">,
|
|
64
|
+
models: ProviderModelConfig[],
|
|
65
|
+
streamSimple?: NonNullable<ProviderConfig["streamSimple"]>,
|
|
66
|
+
): void {
|
|
67
|
+
pi.registerProvider("cursor", createCursorProviderConfig(models, streamSimple));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function registerNestedCursorProvider(pi: CursorExtensionApi, models: ProviderModelConfig[]): void {
|
|
71
|
+
const bridge = registerNestedCursorPiToolBridge(pi);
|
|
72
|
+
const nestedRuntimeId = randomUUID();
|
|
73
|
+
let runtimeContext: CursorProviderRuntimeContext = {
|
|
74
|
+
scopeKey: `__nested_cursor__:${nestedRuntimeId}`,
|
|
75
|
+
cwd: getCursorSessionCwd(),
|
|
76
|
+
sessionFile: undefined,
|
|
77
|
+
projectTrusted: getCursorSessionProjectTrusted(),
|
|
78
|
+
bridge,
|
|
79
|
+
localResume: false,
|
|
80
|
+
nativeToolReplay: false,
|
|
81
|
+
disposeAgentAfterTurn: true,
|
|
82
|
+
};
|
|
83
|
+
pi.on("session_start", (_event, ctx) => {
|
|
84
|
+
const sessionFile = ctx.sessionManager?.getSessionFile?.() ?? undefined;
|
|
85
|
+
const sessionId = ctx.sessionManager?.getSessionId?.() ?? nestedRuntimeId;
|
|
86
|
+
runtimeContext = {
|
|
87
|
+
...runtimeContext,
|
|
88
|
+
scopeKey: sessionFile ?? `__nested_cursor__:${sessionId}`,
|
|
89
|
+
cwd: ctx.cwd,
|
|
90
|
+
sessionFile,
|
|
91
|
+
projectTrusted: ctx.isProjectTrusted?.() === true || runtimeContext.projectTrusted,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
pi.on("session_shutdown", async () => {
|
|
95
|
+
await disposeSessionCursorAgent(runtimeContext.scopeKey);
|
|
96
|
+
});
|
|
97
|
+
registerCursorProvider(pi, models, (model, context, options) =>
|
|
98
|
+
streamCursorLazy(model, context, options, runtimeContext));
|
|
55
99
|
}
|
|
56
100
|
|
|
57
101
|
export default async function (pi: CursorExtensionApi) {
|
|
58
102
|
const factoryClaim = claimCursorExtensionFactory();
|
|
59
|
-
if (factoryClaim.kind === "nested")
|
|
103
|
+
if (factoryClaim.kind === "nested") {
|
|
104
|
+
if (!activeCursorProviderModels) {
|
|
105
|
+
throw new Error("Nested Cursor provider loaded before the owner model catalog was ready");
|
|
106
|
+
}
|
|
107
|
+
registerNestedCursorProvider(pi, activeCursorProviderModels);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
60
110
|
|
|
61
111
|
try {
|
|
62
112
|
// Discover first. A discovery failure must not leave process-global
|
|
@@ -67,6 +117,7 @@ export default async function (pi: CursorExtensionApi) {
|
|
|
67
117
|
fallbackIssue = issue;
|
|
68
118
|
},
|
|
69
119
|
});
|
|
120
|
+
activeCursorProviderModels = models;
|
|
70
121
|
|
|
71
122
|
// Session cwd must register before other session_start listeners that depend on it.
|
|
72
123
|
registerCursorSessionScope(pi);
|