pi-cursor-sdk 0.1.15 → 0.1.16
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 +27 -0
- package/README.md +19 -7
- package/docs/cursor-live-smoke-checklist.md +271 -0
- package/docs/cursor-model-ux-spec.md +12 -3
- package/docs/cursor-native-tool-replay.md +16 -5
- package/package.json +2 -1
- package/src/context.ts +180 -5
- package/src/cursor-bridge-contract.ts +27 -0
- package/src/cursor-live-run-accounting.ts +65 -0
- package/src/cursor-native-tool-display.ts +14 -5
- package/src/cursor-pi-tool-bridge.ts +565 -28
- package/src/cursor-provider.ts +200 -128
- package/src/cursor-question-tool.ts +7 -2
- package/src/cursor-session-agent.ts +372 -0
- package/src/cursor-session-cwd.ts +14 -19
- package/src/cursor-session-scope.ts +65 -0
- package/src/cursor-state.ts +38 -10
- package/src/cursor-usage-accounting.ts +71 -0
- package/src/index.ts +20 -3
package/src/cursor-provider.ts
CHANGED
|
@@ -6,17 +6,34 @@ import {
|
|
|
6
6
|
type Model,
|
|
7
7
|
type SimpleStreamOptions,
|
|
8
8
|
type AssistantMessage,
|
|
9
|
+
type ToolResultMessage,
|
|
9
10
|
} from "@earendil-works/pi-ai";
|
|
10
11
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
12
|
import { Agent, createAgentPlatform } from "@cursor/sdk";
|
|
12
13
|
import type { InteractionUpdate, SDKAgent, SettingSource } from "@cursor/sdk";
|
|
13
14
|
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
14
|
-
import {
|
|
15
|
+
import { buildCursorSendPrompt } from "./context.js";
|
|
16
|
+
import {
|
|
17
|
+
acquireSessionCursorAgent,
|
|
18
|
+
commitSessionAgentSend,
|
|
19
|
+
disposeAllSessionCursorAgents,
|
|
20
|
+
resetSessionCursorAgent,
|
|
21
|
+
} from "./cursor-session-agent.js";
|
|
15
22
|
import {
|
|
16
|
-
getRegisteredCursorPiToolBridge,
|
|
17
23
|
type CursorPiBridgeToolRequest,
|
|
18
24
|
type CursorPiToolBridgeRun,
|
|
19
25
|
} from "./cursor-pi-tool-bridge.js";
|
|
26
|
+
import {
|
|
27
|
+
consumeCursorLiveToolResults,
|
|
28
|
+
createCursorLiveRunAccountingState,
|
|
29
|
+
takeCursorLiveTurnInputTokens,
|
|
30
|
+
type CursorLiveRunAccountingState,
|
|
31
|
+
} from "./cursor-live-run-accounting.js";
|
|
32
|
+
import {
|
|
33
|
+
applyCursorApproximateUsage,
|
|
34
|
+
estimateCursorPromptInputTokens,
|
|
35
|
+
getCursorPromptOptions,
|
|
36
|
+
} from "./cursor-usage-accounting.js";
|
|
20
37
|
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
21
38
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
22
39
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
@@ -64,8 +81,6 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
64
81
|
"Cursor SDK request failed. The API key may be missing, invalid, or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
65
82
|
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
66
83
|
"Cursor SDK request failed because the API key may be invalid or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
67
|
-
const APPROX_CHARS_PER_TOKEN = 4;
|
|
68
|
-
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
69
84
|
const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
|
|
70
85
|
const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
71
86
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
@@ -79,12 +94,18 @@ type CursorLiveQueuedEvent =
|
|
|
79
94
|
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
80
95
|
| { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
|
|
81
96
|
|
|
97
|
+
interface CursorLiveSdkRun {
|
|
98
|
+
cancel(): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
interface CursorLiveRun {
|
|
83
102
|
id: string;
|
|
84
103
|
agent: SDKAgent;
|
|
85
104
|
bridgeRun?: CursorPiToolBridgeRun;
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
sessionBridgeRun?: CursorPiToolBridgeRun;
|
|
106
|
+
sessionAgentScopeKey?: string;
|
|
107
|
+
sdkRun?: CursorLiveSdkRun;
|
|
108
|
+
accounting: CursorLiveRunAccountingState;
|
|
88
109
|
pendingEvents: CursorLiveQueuedEvent[];
|
|
89
110
|
textDeltas: string[];
|
|
90
111
|
emittedText: string;
|
|
@@ -103,6 +124,7 @@ interface CursorLiveTurnState {
|
|
|
103
124
|
partial: AssistantMessage;
|
|
104
125
|
thinkingContentIndex: number;
|
|
105
126
|
textContentIndex: number;
|
|
127
|
+
emittedText: string;
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
let cursorNativeReplayCounter = 0;
|
|
@@ -262,27 +284,6 @@ async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<
|
|
|
262
284
|
}
|
|
263
285
|
}
|
|
264
286
|
|
|
265
|
-
function estimateTextTokens(text: string): number {
|
|
266
|
-
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function estimatePromptInputTokens(prompt: CursorPrompt): number {
|
|
270
|
-
return estimateTextTokens(prompt.text) + prompt.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function getPromptInputTokenBudget(model: Model<Api>): number {
|
|
274
|
-
const outputReserveTokens = Math.min(model.maxTokens, Math.max(1, Math.floor(model.contextWindow * 0.2)));
|
|
275
|
-
return Math.max(1, model.contextWindow - outputReserveTokens);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function setApproximateUsage(partial: AssistantMessage, promptInputTokens: number, outputText: string): void {
|
|
279
|
-
partial.usage.input = promptInputTokens;
|
|
280
|
-
partial.usage.output = estimateTextTokens(outputText);
|
|
281
|
-
partial.usage.cacheRead = 0;
|
|
282
|
-
partial.usage.cacheWrite = 0;
|
|
283
|
-
partial.usage.totalTokens = partial.usage.input + partial.usage.output;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
287
|
function sanitizeSingleLine(value: string): string {
|
|
287
288
|
return value.replace(/\s+/g, " ").trim();
|
|
288
289
|
}
|
|
@@ -391,6 +392,18 @@ function getPendingCursorLiveRun(context: Context): CursorLiveRun | undefined {
|
|
|
391
392
|
return undefined;
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
function isCursorLiveRunToolResult(run: CursorLiveRun, message: ToolResultMessage): boolean {
|
|
396
|
+
const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
|
|
397
|
+
if (replayId) return replayId === run.id;
|
|
398
|
+
return run.bridgeRun?.hasPendingPiToolCallId(message.toolCallId) ?? false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function consumeCursorLiveRunToolResults(run: CursorLiveRun, context: Context) {
|
|
402
|
+
const consumed = consumeCursorLiveToolResults(run.accounting, context, (toolResult) => isCursorLiveRunToolResult(run, toolResult));
|
|
403
|
+
run.accounting = consumed.state;
|
|
404
|
+
return consumed;
|
|
405
|
+
}
|
|
406
|
+
|
|
394
407
|
function splitTextIntoReplayDeltas(text: string): string[] {
|
|
395
408
|
const deltas: string[] = [];
|
|
396
409
|
let remaining = text;
|
|
@@ -446,7 +459,7 @@ function scheduleCursorNativeRunIdleDispose(run: CursorLiveRun): void {
|
|
|
446
459
|
if (run.disposed) return;
|
|
447
460
|
clearCursorNativeRunIdleDispose(run);
|
|
448
461
|
run.idleDisposeTimer = setTimeout(() => {
|
|
449
|
-
void
|
|
462
|
+
void releaseCursorLiveRun(run);
|
|
450
463
|
}, cursorNativeReplayIdleDisposeMs);
|
|
451
464
|
run.idleDisposeTimer.unref?.();
|
|
452
465
|
}
|
|
@@ -557,6 +570,7 @@ function emitCursorLiveQueuedEvent(
|
|
|
557
570
|
} else if (event.type === "thinking-completed") {
|
|
558
571
|
closeCursorNativeThinkingBlock(turn);
|
|
559
572
|
} else if (event.type === "text-delta") {
|
|
573
|
+
turn.emittedText += event.text;
|
|
560
574
|
if (run) run.emittedText += event.text;
|
|
561
575
|
emitCursorNativeTextDelta(turn, event.text);
|
|
562
576
|
}
|
|
@@ -580,44 +594,72 @@ function collectCursorBridgeToolBatch(run: CursorLiveRun): CursorPiBridgeToolReq
|
|
|
580
594
|
return requests;
|
|
581
595
|
}
|
|
582
596
|
|
|
583
|
-
function
|
|
597
|
+
function isCursorTextBoundary(text: string, index: number): boolean {
|
|
598
|
+
if (index <= 0 || index >= text.length) return true;
|
|
599
|
+
const before = text[index - 1];
|
|
600
|
+
const after = text[index];
|
|
601
|
+
return !/[\p{L}\p{N}_]/u.test(before) || !/[\p{L}\p{N}_]/u.test(after);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function trimAlreadyEmittedCursorText(text: string, emittedText: string, options?: { allowPartialPrefix?: boolean }): string {
|
|
584
605
|
if (!text || !emittedText) return text;
|
|
585
606
|
if (text === emittedText) return "";
|
|
586
|
-
if (text.startsWith(emittedText)
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (emittedText.
|
|
607
|
+
if (text.startsWith(emittedText) && (options?.allowPartialPrefix || isCursorTextBoundary(text, emittedText.length))) {
|
|
608
|
+
return text.slice(emittedText.length);
|
|
609
|
+
}
|
|
610
|
+
if (emittedText.endsWith(text) && isCursorTextBoundary(emittedText, emittedText.length - text.length)) return "";
|
|
611
|
+
const trimmedText = text.trim();
|
|
612
|
+
const trimmedEmittedText = emittedText.trim();
|
|
613
|
+
if (trimmedText === trimmedEmittedText) return "";
|
|
614
|
+
if (trimmedText && trimmedEmittedText.endsWith(trimmedText)) {
|
|
615
|
+
const suffixStart = trimmedEmittedText.length - trimmedText.length;
|
|
616
|
+
if (isCursorTextBoundary(trimmedEmittedText, suffixStart)) return "";
|
|
617
|
+
}
|
|
590
618
|
return text;
|
|
591
619
|
}
|
|
592
620
|
|
|
621
|
+
function trimCurrentTurnAlreadyEmittedCursorText(text: string, currentTurnEmittedText: string, emittedText = currentTurnEmittedText): string {
|
|
622
|
+
if (!currentTurnEmittedText) return trimAlreadyEmittedCursorText(text, emittedText);
|
|
623
|
+
const currentTurnTrimmedText = trimAlreadyEmittedCursorText(text, currentTurnEmittedText, { allowPartialPrefix: true });
|
|
624
|
+
if (currentTurnTrimmedText !== text) return currentTurnTrimmedText;
|
|
625
|
+
if (emittedText.endsWith(currentTurnEmittedText)) {
|
|
626
|
+
const emittedTextTrimmedText = trimAlreadyEmittedCursorText(text, emittedText, { allowPartialPrefix: true });
|
|
627
|
+
if (emittedTextTrimmedText !== text) return emittedTextTrimmedText;
|
|
628
|
+
}
|
|
629
|
+
return trimAlreadyEmittedCursorText(text, emittedText);
|
|
630
|
+
}
|
|
631
|
+
|
|
593
632
|
function selectCursorFinalText(
|
|
594
633
|
resultText: unknown,
|
|
595
634
|
textDeltas: readonly string[],
|
|
596
635
|
emittedText: string,
|
|
597
636
|
fallbackText?: string,
|
|
637
|
+
options?: { allowPartialPrefix?: boolean },
|
|
598
638
|
): string {
|
|
599
639
|
const candidates = [typeof resultText === "string" ? resultText : undefined, fallbackText, textDeltas.join("")];
|
|
600
640
|
for (const candidate of candidates) {
|
|
601
641
|
if (!hasUsableText(candidate)) continue;
|
|
602
|
-
const trimmedCandidate = trimAlreadyEmittedCursorText(candidate, emittedText);
|
|
642
|
+
const trimmedCandidate = trimAlreadyEmittedCursorText(candidate, emittedText, options);
|
|
603
643
|
if (hasUsableText(trimmedCandidate)) return trimmedCandidate;
|
|
604
644
|
}
|
|
605
645
|
return "";
|
|
606
646
|
}
|
|
607
647
|
|
|
608
|
-
function
|
|
648
|
+
function takeCursorLiveSessionInputTokens(run: CursorLiveRun, toolResultInputTokens: number): number {
|
|
609
649
|
// Native replay can split one Cursor run into multiple pi turns; count prompt input once.
|
|
610
|
-
|
|
611
|
-
run.
|
|
612
|
-
return
|
|
650
|
+
const taken = takeCursorLiveTurnInputTokens(run.accounting, toolResultInputTokens);
|
|
651
|
+
run.accounting = taken.state;
|
|
652
|
+
return taken.sessionInputTokens;
|
|
613
653
|
}
|
|
614
654
|
|
|
615
655
|
function emitCursorNativeToolUseTurn(
|
|
616
656
|
stream: AssistantMessageEventStream,
|
|
617
657
|
partial: AssistantMessage,
|
|
658
|
+
model: Model<Api>,
|
|
659
|
+
context: Context,
|
|
618
660
|
run: CursorLiveRun,
|
|
661
|
+
toolResultInputTokens: number,
|
|
619
662
|
tools: CursorNativeToolDisplayItem[],
|
|
620
|
-
outputText: string,
|
|
621
663
|
): void {
|
|
622
664
|
const shouldTerminate = run.done && !run.finalText?.trim() && run.pendingEvents.length === 0;
|
|
623
665
|
for (const tool of tools) {
|
|
@@ -636,7 +678,7 @@ function emitCursorNativeToolUseTurn(
|
|
|
636
678
|
run.recordedToolDisplayIds.push(tool.id);
|
|
637
679
|
}
|
|
638
680
|
}
|
|
639
|
-
|
|
681
|
+
applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
|
|
640
682
|
partial.stopReason = "toolUse";
|
|
641
683
|
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
642
684
|
scheduleCursorNativeRunIdleDispose(run);
|
|
@@ -645,9 +687,11 @@ function emitCursorNativeToolUseTurn(
|
|
|
645
687
|
function emitCursorBridgeToolUseTurn(
|
|
646
688
|
stream: AssistantMessageEventStream,
|
|
647
689
|
partial: AssistantMessage,
|
|
690
|
+
model: Model<Api>,
|
|
691
|
+
context: Context,
|
|
648
692
|
run: CursorLiveRun,
|
|
693
|
+
toolResultInputTokens: number,
|
|
649
694
|
requests: CursorPiBridgeToolRequest[],
|
|
650
|
-
outputText: string,
|
|
651
695
|
): void {
|
|
652
696
|
for (const request of requests) {
|
|
653
697
|
const contentIndex = partial.content.length;
|
|
@@ -662,37 +706,58 @@ function emitCursorBridgeToolUseTurn(
|
|
|
662
706
|
const block = partial.content[contentIndex];
|
|
663
707
|
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
664
708
|
}
|
|
665
|
-
|
|
709
|
+
applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
|
|
666
710
|
partial.stopReason = "toolUse";
|
|
667
711
|
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
668
712
|
scheduleCursorNativeRunIdleDispose(run);
|
|
669
713
|
}
|
|
670
714
|
|
|
671
|
-
|
|
715
|
+
function isSuccessfulCursorLiveRun(run: CursorLiveRun): boolean {
|
|
716
|
+
return run.done && !run.cancelled && !run.errorMessage;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function abandonSessionCursorAgent(scopeKey: string | undefined): Promise<void> {
|
|
720
|
+
if (!scopeKey) return;
|
|
721
|
+
await resetSessionCursorAgent(scopeKey);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function releaseCursorLiveRun(run: CursorLiveRun): Promise<void> {
|
|
672
725
|
if (run.disposed) return;
|
|
726
|
+
const abandoned = !isSuccessfulCursorLiveRun(run);
|
|
673
727
|
run.disposed = true;
|
|
674
728
|
pendingCursorLiveRuns.delete(run.id);
|
|
675
729
|
clearCursorNativeRunIdleDispose(run);
|
|
676
|
-
run.bridgeRun?.cancel("Cursor live run
|
|
730
|
+
run.bridgeRun?.cancel("Cursor live run released");
|
|
677
731
|
for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
|
|
678
732
|
run.recordedToolDisplayIds = [];
|
|
679
733
|
run.waiters.clear();
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
} catch {
|
|
683
|
-
// bridge disposal failure should not mask the provider result
|
|
734
|
+
if (run.sessionBridgeRun) {
|
|
735
|
+
run.sessionBridgeRun.setOnToolRequest(undefined);
|
|
684
736
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
737
|
+
if (run.bridgeRun && run.bridgeRun !== run.sessionBridgeRun) {
|
|
738
|
+
try {
|
|
739
|
+
await run.bridgeRun.dispose();
|
|
740
|
+
} catch {
|
|
741
|
+
// bridge disposal failure should not mask the provider result
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (abandoned) {
|
|
745
|
+
try {
|
|
746
|
+
await run.sdkRun?.cancel();
|
|
747
|
+
} catch {
|
|
748
|
+
// cancellation failure should not block session-agent abandonment
|
|
749
|
+
}
|
|
750
|
+
await abandonSessionCursorAgent(run.sessionAgentScopeKey);
|
|
689
751
|
}
|
|
690
752
|
}
|
|
691
753
|
|
|
692
754
|
async function emitCursorNativeRunNextTurn(
|
|
693
755
|
stream: AssistantMessageEventStream,
|
|
694
756
|
partial: AssistantMessage,
|
|
757
|
+
model: Model<Api>,
|
|
758
|
+
context: Context,
|
|
695
759
|
run: CursorLiveRun,
|
|
760
|
+
toolResultInputTokens: number,
|
|
696
761
|
signal?: AbortSignal,
|
|
697
762
|
): Promise<void> {
|
|
698
763
|
const turn: CursorLiveTurnState = {
|
|
@@ -700,6 +765,7 @@ async function emitCursorNativeRunNextTurn(
|
|
|
700
765
|
partial,
|
|
701
766
|
thinkingContentIndex: -1,
|
|
702
767
|
textContentIndex: -1,
|
|
768
|
+
emittedText: "",
|
|
703
769
|
};
|
|
704
770
|
|
|
705
771
|
while (true) {
|
|
@@ -708,17 +774,17 @@ async function emitCursorNativeRunNextTurn(
|
|
|
708
774
|
if (event.type === "tool") {
|
|
709
775
|
await settleCursorLiveToolBatch(run);
|
|
710
776
|
if (signal?.aborted) throw new CursorAbortError();
|
|
711
|
-
|
|
777
|
+
closeCursorNativeTurnBlocks(turn);
|
|
712
778
|
const tools = collectCursorNativeToolBatch(run);
|
|
713
|
-
emitCursorNativeToolUseTurn(stream, partial, run,
|
|
779
|
+
emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, tools);
|
|
714
780
|
return;
|
|
715
781
|
}
|
|
716
782
|
if (event.type === "bridge-tool") {
|
|
717
783
|
await settleCursorLiveToolBatch(run);
|
|
718
784
|
if (signal?.aborted) throw new CursorAbortError();
|
|
719
|
-
|
|
785
|
+
closeCursorNativeTurnBlocks(turn);
|
|
720
786
|
const requests = collectCursorBridgeToolBatch(run);
|
|
721
|
-
emitCursorBridgeToolUseTurn(stream, partial, run,
|
|
787
|
+
emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
|
|
722
788
|
return;
|
|
723
789
|
}
|
|
724
790
|
run.pendingEvents.shift();
|
|
@@ -728,26 +794,26 @@ async function emitCursorNativeRunNextTurn(
|
|
|
728
794
|
if (run.cancelled) {
|
|
729
795
|
partial.stopReason = "aborted";
|
|
730
796
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
731
|
-
await
|
|
797
|
+
await releaseCursorLiveRun(run);
|
|
732
798
|
return;
|
|
733
799
|
}
|
|
734
800
|
if (run.errorMessage) {
|
|
735
801
|
partial.stopReason = "error";
|
|
736
802
|
partial.errorMessage = run.errorMessage;
|
|
737
803
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
738
|
-
await
|
|
804
|
+
await releaseCursorLiveRun(run);
|
|
739
805
|
return;
|
|
740
806
|
}
|
|
741
807
|
if (run.done) {
|
|
742
|
-
|
|
743
|
-
const finalText =
|
|
808
|
+
closeCursorNativeTurnBlocks(turn);
|
|
809
|
+
const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
|
|
744
810
|
if (finalText) {
|
|
745
|
-
|
|
811
|
+
await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
|
|
746
812
|
}
|
|
747
|
-
|
|
813
|
+
applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
|
|
748
814
|
partial.stopReason = "stop";
|
|
749
815
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
750
|
-
await
|
|
816
|
+
await releaseCursorLiveRun(run);
|
|
751
817
|
return;
|
|
752
818
|
}
|
|
753
819
|
|
|
@@ -758,17 +824,19 @@ async function emitCursorNativeRunNextTurn(
|
|
|
758
824
|
async function replayPendingCursorLiveRun(
|
|
759
825
|
stream: AssistantMessageEventStream,
|
|
760
826
|
partial: AssistantMessage,
|
|
827
|
+
model: Model<Api>,
|
|
761
828
|
context: Context,
|
|
762
829
|
signal?: AbortSignal,
|
|
763
830
|
): Promise<boolean> {
|
|
764
831
|
const run = getPendingCursorLiveRun(context);
|
|
765
832
|
if (!run) return false;
|
|
766
833
|
clearCursorNativeRunIdleDispose(run);
|
|
767
|
-
run
|
|
834
|
+
const consumed = consumeCursorLiveRunToolResults(run, context);
|
|
835
|
+
run.bridgeRun?.resolveToolResults(consumed.toolResults);
|
|
768
836
|
try {
|
|
769
|
-
await emitCursorNativeRunNextTurn(stream, partial, run, signal);
|
|
837
|
+
await emitCursorNativeRunNextTurn(stream, partial, model, context, run, consumed.toolResultInputTokens, signal);
|
|
770
838
|
} catch (error) {
|
|
771
|
-
if (error instanceof CursorAbortError) await
|
|
839
|
+
if (error instanceof CursorAbortError) await releaseCursorLiveRun(run);
|
|
772
840
|
throw error;
|
|
773
841
|
}
|
|
774
842
|
return true;
|
|
@@ -786,10 +854,10 @@ export function streamCursor(
|
|
|
786
854
|
let agent: SDKAgent | null = null;
|
|
787
855
|
let activeLiveRun: CursorLiveRun | undefined;
|
|
788
856
|
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
789
|
-
let bridgeRunOwnedByLiveRun = false;
|
|
790
857
|
let liveRunForBridgeQueue: CursorLiveRun | undefined;
|
|
791
858
|
const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
|
|
792
859
|
let resolvedApiKey: string | undefined;
|
|
860
|
+
let sessionAgentScopeKey = "";
|
|
793
861
|
let abortSignal: AbortSignal | undefined;
|
|
794
862
|
let abortListener: (() => void) | undefined;
|
|
795
863
|
let restoreCursorSdkOutputFilter: (() => void) | undefined;
|
|
@@ -802,7 +870,7 @@ export function streamCursor(
|
|
|
802
870
|
stream.push({ type: "start", partial });
|
|
803
871
|
throwIfAborted();
|
|
804
872
|
|
|
805
|
-
if (await replayPendingCursorLiveRun(stream, partial, context, options?.signal)) {
|
|
873
|
+
if (await replayPendingCursorLiveRun(stream, partial, model, context, options?.signal)) {
|
|
806
874
|
stream.end();
|
|
807
875
|
return;
|
|
808
876
|
}
|
|
@@ -818,41 +886,41 @@ export function streamCursor(
|
|
|
818
886
|
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
819
887
|
const settingSources = resolveCursorSettingSources();
|
|
820
888
|
|
|
821
|
-
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
822
|
-
bridgeRun = registeredBridge
|
|
823
|
-
? await registeredBridge.createRun({
|
|
824
|
-
onToolRequest: (request) => {
|
|
825
|
-
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
826
|
-
queueCursorNativeEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
827
|
-
} else {
|
|
828
|
-
queuedBridgeRequestsBeforeLiveRun.push(request);
|
|
829
|
-
}
|
|
830
|
-
},
|
|
831
|
-
})
|
|
832
|
-
: undefined;
|
|
833
|
-
if (!bridgeRun?.enabled || !bridgeRun.mcpServers) {
|
|
834
|
-
await bridgeRun?.dispose();
|
|
835
|
-
bridgeRun = undefined;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
889
|
installCursorMcpToolTimeoutOverride();
|
|
839
890
|
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
891
|
+
const sessionAgentAcquireParams = {
|
|
892
|
+
apiKey,
|
|
893
|
+
cwd,
|
|
894
|
+
modelSelection: selection,
|
|
895
|
+
settingSources,
|
|
896
|
+
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
897
|
+
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
898
|
+
queueCursorNativeEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
|
|
899
|
+
} else {
|
|
900
|
+
queuedBridgeRequestsBeforeLiveRun.push(request);
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
createAgent: (createOptions: Parameters<typeof Agent.create>[0]) =>
|
|
904
|
+
suppressCursorSdkOutput(() => Agent.create(createOptions)),
|
|
905
|
+
};
|
|
906
|
+
let sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
907
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
908
|
+
agent = sessionAgentLease.agent;
|
|
909
|
+
bridgeRun = sessionAgentLease.bridgeRun;
|
|
848
910
|
throwIfAborted();
|
|
849
911
|
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
912
|
+
const promptOptions = getCursorPromptOptions(model);
|
|
913
|
+
let { prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState);
|
|
914
|
+
if (sessionAgentLease.sendState.bootstrapped && bootstrap) {
|
|
915
|
+
await resetSessionCursorAgent(sessionAgentLease.scopeKey);
|
|
916
|
+
sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
917
|
+
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
918
|
+
agent = sessionAgentLease.agent;
|
|
919
|
+
bridgeRun = sessionAgentLease.bridgeRun;
|
|
920
|
+
({ prompt, bootstrap } = buildCursorSendPrompt(context, promptOptions, sessionAgentLease.sendState));
|
|
921
|
+
}
|
|
922
|
+
const sessionBridgeRun = sessionAgentLease.bridgeRun;
|
|
923
|
+
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
856
924
|
let thinkingContentIndex = -1;
|
|
857
925
|
let activityTraceChars = 0;
|
|
858
926
|
let activityTraceTruncated = false;
|
|
@@ -868,8 +936,9 @@ export function streamCursor(
|
|
|
868
936
|
id: useNativeToolReplay ? nativeReplayId : bridgeRun!.id,
|
|
869
937
|
agent,
|
|
870
938
|
bridgeRun,
|
|
871
|
-
|
|
872
|
-
|
|
939
|
+
sessionBridgeRun,
|
|
940
|
+
sessionAgentScopeKey,
|
|
941
|
+
accounting: createCursorLiveRunAccountingState(promptInputTokens),
|
|
873
942
|
pendingEvents: [],
|
|
874
943
|
textDeltas,
|
|
875
944
|
emittedText: "",
|
|
@@ -884,7 +953,6 @@ export function streamCursor(
|
|
|
884
953
|
pendingCursorLiveRuns.set(liveRun.id, liveRun);
|
|
885
954
|
activeLiveRun = liveRun;
|
|
886
955
|
liveRunForBridgeQueue = liveRun;
|
|
887
|
-
bridgeRunOwnedByLiveRun = bridgeRun !== undefined;
|
|
888
956
|
for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
|
|
889
957
|
queueCursorNativeEvent(liveRun, { type: "bridge-tool", request });
|
|
890
958
|
}
|
|
@@ -1218,6 +1286,7 @@ export function streamCursor(
|
|
|
1218
1286
|
{ text: prompt.text, images: prompt.images.length > 0 ? prompt.images : undefined },
|
|
1219
1287
|
{ onDelta, onStep },
|
|
1220
1288
|
);
|
|
1289
|
+
if (liveRun) liveRun.sdkRun = run;
|
|
1221
1290
|
if (options?.signal?.aborted) {
|
|
1222
1291
|
await run.cancel().catch(() => {});
|
|
1223
1292
|
throw new CursorAbortError();
|
|
@@ -1231,14 +1300,24 @@ export function streamCursor(
|
|
|
1231
1300
|
discardIncompleteStartedToolCalls();
|
|
1232
1301
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
1233
1302
|
if (liveRun.disposed) return;
|
|
1303
|
+
if (result.status === "finished" && !options?.signal?.aborted) {
|
|
1304
|
+
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
1305
|
+
} else {
|
|
1306
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1307
|
+
}
|
|
1234
1308
|
liveRun.cancelled = result.status === "cancelled";
|
|
1235
|
-
|
|
1309
|
+
if (result.status === "error") {
|
|
1310
|
+
liveRun.errorMessage = sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey);
|
|
1311
|
+
} else {
|
|
1312
|
+
liveRun.finalText = selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, cursorPlanTextCandidate);
|
|
1313
|
+
}
|
|
1236
1314
|
liveRun.done = true;
|
|
1237
1315
|
notifyCursorNativeRun(liveRun);
|
|
1238
1316
|
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
1239
1317
|
})
|
|
1240
|
-
.catch((error: unknown) => {
|
|
1318
|
+
.catch(async (error: unknown) => {
|
|
1241
1319
|
if (liveRun.disposed) return;
|
|
1320
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1242
1321
|
liveRun.errorMessage = sanitizeError(error, resolvedApiKey ?? options?.apiKey);
|
|
1243
1322
|
notifyCursorNativeRun(liveRun);
|
|
1244
1323
|
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
@@ -1248,9 +1327,9 @@ export function streamCursor(
|
|
|
1248
1327
|
await waitForCursorNativeRunProgress(liveRun, options?.signal);
|
|
1249
1328
|
await settleCursorLiveToolBatch(liveRun);
|
|
1250
1329
|
closeTraceBlock();
|
|
1251
|
-
await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
|
|
1330
|
+
await emitCursorNativeRunNextTurn(stream, partial, model, context, liveRun, 0, options?.signal);
|
|
1252
1331
|
} catch (error) {
|
|
1253
|
-
if (error instanceof CursorAbortError) await
|
|
1332
|
+
if (error instanceof CursorAbortError) await releaseCursorLiveRun(liveRun);
|
|
1254
1333
|
throw error;
|
|
1255
1334
|
}
|
|
1256
1335
|
agent = null;
|
|
@@ -1266,16 +1345,26 @@ export function streamCursor(
|
|
|
1266
1345
|
closeTraceBlock();
|
|
1267
1346
|
|
|
1268
1347
|
if (result.status === "cancelled") {
|
|
1348
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1269
1349
|
partial.stopReason = "aborted";
|
|
1270
1350
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
1351
|
+
} else if (result.status === "error") {
|
|
1352
|
+
await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1353
|
+
partial.stopReason = "error";
|
|
1354
|
+
partial.errorMessage = sanitizeError(result.result ?? "Cursor SDK run failed", resolvedApiKey ?? options?.apiKey);
|
|
1355
|
+
stream.push({ type: "error", reason: "error", error: partial });
|
|
1271
1356
|
} else {
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1357
|
+
commitSessionAgentSend(sessionAgentScopeKey, context, bootstrap);
|
|
1358
|
+
const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""), cursorPlanTextCandidate, {
|
|
1359
|
+
allowPartialPrefix: true,
|
|
1360
|
+
});
|
|
1361
|
+
flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
|
|
1362
|
+
applyCursorApproximateUsage(partial, model, context, promptInputTokens);
|
|
1275
1363
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
1276
1364
|
}
|
|
1277
1365
|
} catch (error) {
|
|
1278
|
-
if (activeLiveRun && !activeLiveRun.disposed) await
|
|
1366
|
+
if (activeLiveRun && !activeLiveRun.disposed) await releaseCursorLiveRun(activeLiveRun);
|
|
1367
|
+
else await abandonSessionCursorAgent(sessionAgentScopeKey);
|
|
1279
1368
|
if (error instanceof CursorAbortError) {
|
|
1280
1369
|
partial.stopReason = "aborted";
|
|
1281
1370
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
@@ -1286,28 +1375,10 @@ export function streamCursor(
|
|
|
1286
1375
|
}
|
|
1287
1376
|
} finally {
|
|
1288
1377
|
restoreCursorSdkOutputFilter?.();
|
|
1289
|
-
if (activeLiveRun?.disposed) agent = null;
|
|
1290
1378
|
|
|
1291
1379
|
if (abortSignal && abortListener) {
|
|
1292
1380
|
abortSignal.removeEventListener("abort", abortListener);
|
|
1293
1381
|
}
|
|
1294
|
-
|
|
1295
|
-
if (bridgeRun && !bridgeRunOwnedByLiveRun) {
|
|
1296
|
-
try {
|
|
1297
|
-
await bridgeRun.dispose();
|
|
1298
|
-
} catch {
|
|
1299
|
-
// bridge disposal failure should not mask original error
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
if (agent) {
|
|
1304
|
-
try {
|
|
1305
|
-
await agent[Symbol.asyncDispose]();
|
|
1306
|
-
} catch {
|
|
1307
|
-
// disposal failure should not mask original error
|
|
1308
|
-
}
|
|
1309
|
-
agent = null;
|
|
1310
|
-
}
|
|
1311
1382
|
}
|
|
1312
1383
|
|
|
1313
1384
|
stream.end();
|
|
@@ -1325,4 +1396,5 @@ export const __testUtils = {
|
|
|
1325
1396
|
resetCursorNativeReplayIdleDisposeMs: () => {
|
|
1326
1397
|
cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
1327
1398
|
},
|
|
1399
|
+
resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
|
|
1328
1400
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge.js";
|
|
@@ -34,6 +34,11 @@ interface CursorQuestionDetails {
|
|
|
34
34
|
cancelled: boolean;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> {
|
|
38
|
+
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
39
|
+
on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
type RawQuestionOption = string | { label?: string; value?: string; description?: string };
|
|
38
43
|
|
|
39
44
|
type RawQuestion = {
|
|
@@ -186,7 +191,7 @@ function syncCursorQuestionToolForModel(pi: Pick<ExtensionAPI, "getActiveTools"
|
|
|
186
191
|
pi.setActiveTools([...activeToolNames]);
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
export function registerCursorQuestionTool(pi:
|
|
194
|
+
export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi): void {
|
|
190
195
|
pi.registerTool({
|
|
191
196
|
name: CURSOR_ASK_QUESTION_TOOL_NAME,
|
|
192
197
|
label: "Cursor question",
|