pi-cursor-sdk 0.2.0 → 0.3.7
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 +73 -0
- package/README.md +55 -51
- package/dist/bundled-context-windows.js +39 -0
- package/dist/context-window-cache.js +92 -0
- package/dist/context.js +353 -0
- package/dist/cursor-active-tools.js +3 -0
- package/dist/cursor-agent-message-web-tools.js +165 -0
- package/dist/cursor-agents-context-registration.js +17 -0
- package/dist/cursor-agents-context.js +127 -0
- package/dist/cursor-api-key.js +34 -0
- package/dist/cursor-bridge-contract.js +27 -0
- package/dist/cursor-cloud-lifecycle.js +650 -0
- package/dist/cursor-cloud-local-state.js +460 -0
- package/dist/cursor-cloud-options.js +145 -0
- package/dist/cursor-cloud-reporting.js +222 -0
- package/dist/cursor-compact-tool-summary.js +75 -0
- package/dist/cursor-config.js +613 -0
- package/dist/cursor-context-tools.js +6 -0
- package/dist/cursor-display-only-trace.js +11 -0
- package/dist/cursor-display-text.js +18 -0
- package/dist/cursor-durable-fs.js +53 -0
- package/dist/cursor-edit-diff.js +13 -0
- package/dist/cursor-env-boolean.js +19 -0
- package/dist/cursor-fallback-models.generated.js +7760 -0
- package/dist/cursor-fallback-warning.js +15 -0
- package/dist/cursor-http1.js +45 -0
- package/dist/cursor-incomplete-tool-visibility.js +83 -0
- package/dist/cursor-live-run-accounting.js +55 -0
- package/dist/cursor-live-run-coordinator.js +409 -0
- package/dist/cursor-mcp-timeout-override.js +116 -0
- package/dist/cursor-model-lifecycle.js +32 -0
- package/dist/cursor-model.js +5 -0
- package/dist/cursor-native-replay-routing.js +35 -0
- package/dist/cursor-native-replay-trace.js +28 -0
- package/dist/cursor-native-tool-display-registration.js +102 -0
- package/dist/cursor-native-tool-display-replay.js +428 -0
- package/dist/cursor-native-tool-display-state.js +70 -0
- package/dist/cursor-native-tool-display-tools.js +123 -0
- package/dist/cursor-native-tool-names.js +11 -0
- package/dist/cursor-partial-content-emitter.js +130 -0
- package/dist/cursor-pi-context.js +30 -0
- package/dist/cursor-pi-tool-bridge-abort.js +114 -0
- package/dist/cursor-pi-tool-bridge-constants.js +6 -0
- package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
- package/dist/cursor-pi-tool-bridge-env.js +18 -0
- package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
- package/dist/cursor-pi-tool-bridge-run.js +355 -0
- package/dist/cursor-pi-tool-bridge-server.js +163 -0
- package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
- package/dist/cursor-pi-tool-bridge-types.js +1 -0
- package/dist/cursor-pi-tool-bridge.js +123 -0
- package/dist/cursor-provider-errors.js +311 -0
- package/dist/cursor-provider-lazy.js +39 -0
- package/dist/cursor-provider-live-run-drain.js +420 -0
- package/dist/cursor-provider-overflow.js +90 -0
- package/dist/cursor-provider-run-finalizer.js +176 -0
- package/dist/cursor-provider-run-outcome.js +76 -0
- package/dist/cursor-provider-turn-coordinator.js +293 -0
- package/dist/cursor-provider-turn-display-router.js +158 -0
- package/dist/cursor-provider-turn-emit.js +41 -0
- package/dist/cursor-provider-turn-finalize.js +154 -0
- package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
- package/dist/cursor-provider-turn-prepare.js +379 -0
- package/dist/cursor-provider-turn-runner.js +146 -0
- package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
- package/dist/cursor-provider-turn-send.js +138 -0
- package/dist/cursor-provider-turn-shell-output.js +117 -0
- package/dist/cursor-provider-turn-tool-ledger.js +99 -0
- package/dist/cursor-provider-turn-types.js +1 -0
- package/dist/cursor-provider.js +72 -0
- package/dist/cursor-question-tool.js +194 -0
- package/dist/cursor-record-utils.js +61 -0
- package/dist/cursor-replay-activity-builders.js +114 -0
- package/dist/cursor-replay-source-names.js +27 -0
- package/dist/cursor-replay-summary-args.js +105 -0
- package/dist/cursor-replay-tool-details.js +167 -0
- package/dist/cursor-ripgrep-path.js +27 -0
- package/dist/cursor-run-final-text.js +63 -0
- package/dist/cursor-runtime-state.js +380 -0
- package/dist/cursor-sdk-billed-usage.js +86 -0
- package/dist/cursor-sdk-event-debug-constants.js +33 -0
- package/dist/cursor-sdk-event-debug-session.js +107 -0
- package/dist/cursor-sdk-event-debug.js +461 -0
- package/dist/cursor-sdk-output-filter.js +1 -0
- package/dist/cursor-sdk-process-error-guard.js +221 -0
- package/dist/cursor-sdk-runtime.js +3 -0
- package/dist/cursor-sensitive-text.js +23 -0
- package/dist/cursor-session-agent-cleanup.js +311 -0
- package/dist/cursor-session-agent-lifecycle.js +32 -0
- package/dist/cursor-session-agent-lineage.js +103 -0
- package/dist/cursor-session-agent-resume.js +417 -0
- package/dist/cursor-session-agent.js +507 -0
- package/dist/cursor-session-compaction-prep.js +21 -0
- package/dist/cursor-session-scope.js +113 -0
- package/dist/cursor-session-send-policy.js +18 -0
- package/dist/cursor-session-store.js +105 -0
- package/dist/cursor-session-turn-queue.js +50 -0
- package/dist/cursor-setting-sources.js +15 -0
- package/dist/cursor-skill-tool.js +230 -0
- package/dist/cursor-state.js +519 -0
- package/dist/cursor-task-presentation.js +59 -0
- package/dist/cursor-tool-lifecycle.js +94 -0
- package/dist/cursor-tool-manifest.js +34 -0
- package/dist/cursor-tool-presentation-registry.js +305 -0
- package/dist/cursor-tool-result-display-readers.js +158 -0
- package/dist/cursor-tool-transcript.js +32 -0
- package/dist/cursor-tool-visibility.js +28 -0
- package/dist/cursor-transcript-tool-formatters.js +618 -0
- package/dist/cursor-transcript-tool-specs.js +284 -0
- package/dist/cursor-transcript-utils.js +170 -0
- package/dist/cursor-usage-accounting.js +166 -0
- package/dist/cursor-web-tool-activity.js +23 -0
- package/dist/cursor-web-tool-args.js +12 -0
- package/dist/index.js +82 -0
- package/dist/model-discovery.js +305 -0
- package/dist/model-list-cache.js +156 -0
- package/docs/cursor-dogfood-checklist.md +2 -1
- package/docs/cursor-live-smoke-checklist.md +13 -12
- package/docs/cursor-model-ux-spec.md +27 -6
- package/docs/cursor-native-tool-replay.md +13 -13
- package/docs/cursor-native-tool-visual-audit.md +6 -3
- package/docs/cursor-testing-lessons.md +14 -8
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +3 -3
- package/package.json +19 -7
- package/platform-smoke.config.mjs +1 -1
- package/scripts/build.mjs +126 -0
- package/scripts/cloud-runtime-smoke.mjs +3 -1
- package/scripts/debug-provider-events.mjs +3 -1
- package/scripts/debug-sdk-events.mjs +1 -1
- package/scripts/isolated-cursor-smoke.sh +5 -4
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
- package/scripts/lib/cursor-visual-render.mjs +1 -1
- package/scripts/lib/ensure-built.mjs +16 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
- package/scripts/local-resume-smoke.mjs +3 -1
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/render-ansi.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/prepare.mjs +102 -0
- package/scripts/steering-rpc-smoke.mjs +4 -2
- package/scripts/tmux-live-smoke.sh +11 -11
- package/scripts/visual-tui-smoke-self-test.mjs +4 -1
- package/scripts/visual-tui-smoke.mjs +51 -7
- package/src/bundled-context-windows.ts +4 -1
- package/src/context.ts +27 -4
- package/src/cursor-agents-context-registration.ts +2 -2
- package/src/cursor-agents-context.ts +24 -10
- package/src/cursor-api-key.ts +5 -4
- package/src/cursor-cloud-reporting.ts +23 -2
- package/src/cursor-context-tools.ts +3 -1
- package/src/cursor-fallback-models.generated.ts +592 -204
- package/src/cursor-live-run-coordinator.ts +11 -6
- package/src/cursor-native-tool-display-registration.ts +10 -11
- package/src/cursor-partial-content-emitter.ts +13 -0
- package/src/cursor-pi-context.ts +44 -0
- package/src/cursor-provider-errors.ts +1 -1
- package/src/cursor-provider-lazy.ts +5 -4
- package/src/cursor-provider-live-run-drain.ts +14 -9
- package/src/cursor-provider-run-finalizer.ts +1 -0
- package/src/cursor-provider-turn-coordinator.ts +15 -0
- package/src/cursor-provider-turn-emit.ts +6 -0
- package/src/cursor-provider-turn-finalize.ts +12 -0
- package/src/cursor-provider-turn-prepare.ts +10 -5
- package/src/cursor-provider-turn-types.ts +2 -0
- package/src/cursor-sdk-billed-usage.ts +107 -0
- package/src/cursor-sdk-process-error-guard.ts +6 -4
- package/src/cursor-session-agent-lifecycle.ts +8 -9
- package/src/cursor-session-agent-resume.ts +23 -0
- package/src/cursor-session-compaction-prep.ts +4 -0
- package/src/cursor-skill-tool.ts +1 -1
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-usage-accounting.ts +70 -9
- package/src/index.ts +1 -1
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +16 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { getCursorConversationMessages } from "./cursor-pi-context.js";
|
|
2
3
|
import type { SDKAgent } from "@cursor/sdk";
|
|
3
4
|
import {
|
|
4
5
|
consumeCursorLiveToolResults,
|
|
@@ -27,6 +28,7 @@ export type CursorLiveQueuedEvent =
|
|
|
27
28
|
| { type: "thinking-delta"; text: string }
|
|
28
29
|
| { type: "thinking-completed" }
|
|
29
30
|
| { type: "text-delta"; text: string }
|
|
31
|
+
| { type: "text-completed" }
|
|
30
32
|
| { type: "tool"; tool: CursorNativeToolDisplayItem }
|
|
31
33
|
| { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
|
|
32
34
|
|
|
@@ -43,6 +45,7 @@ export interface CursorLiveRun {
|
|
|
43
45
|
sdkRun?: CursorLiveSdkRun;
|
|
44
46
|
ignoreFutureSdkTurnUsage?: boolean;
|
|
45
47
|
accounting: CursorLiveRunAccountingState;
|
|
48
|
+
billedTurnUsage?: CursorSdkTurnUsage;
|
|
46
49
|
pendingEvents: CursorLiveQueuedEvent[];
|
|
47
50
|
textDeltas: string[];
|
|
48
51
|
emittedText: string;
|
|
@@ -154,16 +157,17 @@ interface CursorLiveRunPrivateState {
|
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
export function hasTrailingUserMessagesAfterToolResults(context: Context): boolean {
|
|
157
|
-
|
|
160
|
+
const messages = getCursorConversationMessages(context);
|
|
161
|
+
let index = messages.length - 1;
|
|
158
162
|
let sawTrailingUser = false;
|
|
159
|
-
while (index >= 0 &&
|
|
163
|
+
while (index >= 0 && messages[index]?.role === "user") {
|
|
160
164
|
sawTrailingUser = true;
|
|
161
165
|
index -= 1;
|
|
162
166
|
}
|
|
163
167
|
if (!sawTrailingUser) return false;
|
|
164
168
|
|
|
165
169
|
let sawToolResult = false;
|
|
166
|
-
while (index >= 0 &&
|
|
170
|
+
while (index >= 0 && messages[index]?.role === "toolResult") {
|
|
167
171
|
sawToolResult = true;
|
|
168
172
|
index -= 1;
|
|
169
173
|
}
|
|
@@ -417,13 +421,14 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
417
421
|
},
|
|
418
422
|
|
|
419
423
|
getPendingFromContext(context, getReplayId): CursorLiveRun | undefined {
|
|
420
|
-
|
|
421
|
-
|
|
424
|
+
const messages = getCursorConversationMessages(context);
|
|
425
|
+
let index = messages.length - 1;
|
|
426
|
+
while (index >= 0 && messages[index]?.role === "user") {
|
|
422
427
|
index -= 1;
|
|
423
428
|
}
|
|
424
429
|
|
|
425
430
|
for (; index >= 0; index -= 1) {
|
|
426
|
-
const message =
|
|
431
|
+
const message = messages[index];
|
|
427
432
|
if (message.role !== "toolResult") break;
|
|
428
433
|
const replayId = getReplayId(message.toolCallId);
|
|
429
434
|
if (replayId) {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
skippedNativeToolNames,
|
|
19
19
|
} from "./cursor-native-tool-display-state.js";
|
|
20
20
|
import { isCursorReplayToolName } from "./cursor-tool-presentation-registry.js";
|
|
21
|
+
import { registerNativeCursorTool } from "./cursor-native-tool-display-tools.js";
|
|
21
22
|
|
|
22
23
|
export const CURSOR_CORE_PI_REPLAY_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
|
|
23
24
|
const CORE_PI_TOOL_NAMES = new Set<string>(CURSOR_CORE_PI_REPLAY_TOOL_NAMES);
|
|
@@ -40,12 +41,11 @@ type NativeRegistrationContext = Pick<ExtensionContext, "mode" | "model"> & {
|
|
|
40
41
|
ui: Pick<ExtensionContext["ui"], "notify">;
|
|
41
42
|
};
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
function registerNativeCursorToolsFromSet(
|
|
44
45
|
pi: CursorNativeToolRegistryApi,
|
|
45
46
|
toolNames: readonly NativeCursorToolName[],
|
|
46
|
-
):
|
|
47
|
+
): NativeCursorToolName[] {
|
|
47
48
|
const newlySkippedToolNames: NativeCursorToolName[] = [];
|
|
48
|
-
let registerNativeCursorTool: ((pi: CursorNativeToolRegistryApi, toolName: NativeCursorToolName) => void) | undefined;
|
|
49
49
|
for (const toolName of toolNames) {
|
|
50
50
|
if (registeredNativeToolNames.has(toolName) || skippedNativeToolNames.has(toolName)) continue;
|
|
51
51
|
if (hasNonBuiltinTool(pi, toolName)) {
|
|
@@ -53,7 +53,6 @@ async function registerNativeCursorToolsFromSet(
|
|
|
53
53
|
newlySkippedToolNames.push(toolName);
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
-
registerNativeCursorTool ??= (await import("./cursor-native-tool-display-tools.js")).registerNativeCursorTool;
|
|
57
56
|
registerNativeCursorTool(pi, toolName);
|
|
58
57
|
registeredNativeToolNames.add(toolName);
|
|
59
58
|
}
|
|
@@ -105,31 +104,31 @@ export function syncRegisteredNativeCursorToolsForModel(
|
|
|
105
104
|
if (changed) pi.setActiveTools([...activeToolNames]);
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
|
|
107
|
+
function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
|
|
109
108
|
if (!isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
|
|
110
109
|
|
|
111
110
|
const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
|
|
112
111
|
const skippedToolNames = [
|
|
113
|
-
...
|
|
114
|
-
...
|
|
112
|
+
...registerNativeCursorToolsFromSet(pi, nonCoreToolNames),
|
|
113
|
+
...registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES),
|
|
115
114
|
];
|
|
116
115
|
notifySkippedNativeCursorToolsIfNeeded(ctx, skippedToolNames);
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
|
|
118
|
+
function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
|
|
120
119
|
const requested = isCursorNativeToolRegistrationRequested(ctx.mode);
|
|
121
120
|
setCursorNativeToolDisplayRuntimeRequested(requested);
|
|
122
121
|
if (!requested) {
|
|
123
122
|
removeRegisteredNonCoreNativeCursorTools(pi);
|
|
124
123
|
return;
|
|
125
124
|
}
|
|
126
|
-
|
|
125
|
+
ensureNativeCursorToolsRegisteredForModel(pi, ctx);
|
|
127
126
|
syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
|
|
128
127
|
}
|
|
129
128
|
|
|
130
129
|
export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExtensionApi): void {
|
|
131
|
-
registerCursorModelLifecycle(pi,
|
|
132
|
-
|
|
130
|
+
registerCursorModelLifecycle(pi, (ctx) => {
|
|
131
|
+
ensureThenSyncNativeCursorToolsForModel(pi, ctx);
|
|
133
132
|
});
|
|
134
133
|
}
|
|
135
134
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage, AssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
3
|
const DEFAULT_THINKING_TRACE_MAX_CHARS = 50000;
|
|
4
|
+
export const CURSOR_TEXT_MESSAGE_SEPARATOR = "\n\n";
|
|
4
5
|
|
|
5
6
|
export interface CursorPartialContentEmitterOptions {
|
|
6
7
|
stream: AssistantMessageEventStream;
|
|
@@ -12,6 +13,7 @@ export interface CursorPartialContentEmitterOptions {
|
|
|
12
13
|
export class CursorPartialContentEmitter {
|
|
13
14
|
private thinkingContentIndex = -1;
|
|
14
15
|
private textContentIndex = -1;
|
|
16
|
+
private textMessageCompleted = false;
|
|
15
17
|
private activityTraceChars = 0;
|
|
16
18
|
private activityTraceTruncated = false;
|
|
17
19
|
|
|
@@ -86,10 +88,21 @@ export class CursorPartialContentEmitter {
|
|
|
86
88
|
});
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
completeTextMessage(): void {
|
|
92
|
+
// Defer the separator until more text arrives: an exact final answer must
|
|
93
|
+
// not gain trailing whitespace. A fresh Pi tool-use turn needs no prefix.
|
|
94
|
+
this.textMessageCompleted = this.partial.content.some((block) => block.type === "text" && block.text.length > 0);
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
appendTextDelta(delta: string, options?: { closeThinking?: boolean }): void {
|
|
90
98
|
const closeThinking = options?.closeThinking ?? this.mutuallyExclusive;
|
|
91
99
|
if (closeThinking) this.closeThinking();
|
|
92
100
|
if (!delta) return;
|
|
101
|
+
if (this.textMessageCompleted) {
|
|
102
|
+
this.textMessageCompleted = false;
|
|
103
|
+
this.appendTextDelta(CURSOR_TEXT_MESSAGE_SEPARATOR, { closeThinking: false });
|
|
104
|
+
this.closeText();
|
|
105
|
+
}
|
|
93
106
|
if (this.textContentIndex < 0) {
|
|
94
107
|
this.textContentIndex = this.partial.content.length;
|
|
95
108
|
this.partial.content.push({ type: "text", text: "" });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
2
|
+
import type { Context, Message, Tool } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
// Stock 0.84/0.85 do not export transcript helpers. Keep the namespace import in
|
|
5
|
+
// Pi's static extension graph: the host loader supplies its own peer module.
|
|
6
|
+
// Context remains the common structural input (its shorthand fields are optional);
|
|
7
|
+
// transcript hosts pass messages only. Never implement Pi's replay rules here.
|
|
8
|
+
interface PiTranscriptHelpers {
|
|
9
|
+
normalizeContext(context: Context): { messages: readonly { role: string }[] };
|
|
10
|
+
getCurrentSystemPrompt(messages: readonly { role: string }[]): string;
|
|
11
|
+
getCurrentTools(messages: readonly { role: string }[]): Tool[];
|
|
12
|
+
}
|
|
13
|
+
const optionalHelpers = piAi as typeof piAi & Partial<PiTranscriptHelpers>;
|
|
14
|
+
const transcriptHelpers = typeof optionalHelpers.normalizeContext === "function"
|
|
15
|
+
&& typeof optionalHelpers.getCurrentSystemPrompt === "function"
|
|
16
|
+
&& typeof optionalHelpers.getCurrentTools === "function"
|
|
17
|
+
? optionalHelpers as PiTranscriptHelpers : undefined;
|
|
18
|
+
|
|
19
|
+
export function isCursorSystemMessage(message: { role: string }): boolean {
|
|
20
|
+
return message.role === "system";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Conversation-only view for Cursor text/history and trailing tool-result scans. */
|
|
24
|
+
export function getCursorConversationMessages(context: Pick<Context, "messages">): Message[] {
|
|
25
|
+
return context.messages.filter((message) => !isCursorSystemMessage(message));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveCursorPiContext(context: Context): { systemPrompt: string; tools: Tool[] | undefined } {
|
|
29
|
+
if (!transcriptHelpers) {
|
|
30
|
+
if (context.messages.some(isCursorSystemMessage)) {
|
|
31
|
+
throw new Error("Pi transcript context requires the host's public transcript replay helpers.");
|
|
32
|
+
}
|
|
33
|
+
return { systemPrompt: context.systemPrompt ?? "", tools: context.tools };
|
|
34
|
+
}
|
|
35
|
+
const messages = transcriptHelpers.normalizeContext(context).messages;
|
|
36
|
+
const legacyWithoutTools = ("systemPrompt" in context || "tools" in context)
|
|
37
|
+
&& context.tools === undefined && !context.messages.some(isCursorSystemMessage);
|
|
38
|
+
return {
|
|
39
|
+
systemPrompt: transcriptHelpers.getCurrentSystemPrompt(messages),
|
|
40
|
+
// A messages-only provider request is authoritative, including no declarations.
|
|
41
|
+
// An explicitly legacy shorthand context retains its absent-snapshot semantics.
|
|
42
|
+
tools: legacyWithoutTools ? undefined : transcriptHelpers.getCurrentTools(messages),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -166,7 +166,7 @@ function isCursorSdkStallAbortNetworkError(code: unknown, evidence: string, stac
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* @cursor/sdk@1.0.
|
|
169
|
+
* @cursor/sdk@1.0.27 throws internal RetriableError (name/kind "RetriableError") with message
|
|
170
170
|
* "Connection stalled" or "Connection stalled repeatedly" after fetchWithRetry exhausts stalls.
|
|
171
171
|
* The ConnectError is only the cause; the top-level error is not a ConnectError.
|
|
172
172
|
*/
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
type Model,
|
|
8
8
|
type SimpleStreamOptions,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
|
+
import { streamCursor } from "./cursor-provider.js";
|
|
11
|
+
import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
|
|
10
12
|
|
|
11
|
-
function
|
|
13
|
+
function makeProviderRuntimeErrorMessage(model: Model<Api>, error: unknown, apiKey?: string): AssistantMessage {
|
|
12
14
|
return {
|
|
13
15
|
role: "assistant",
|
|
14
16
|
content: [],
|
|
@@ -25,7 +27,7 @@ function makeProviderLoadErrorMessage(model: Model<Api>, error: unknown): Assist
|
|
|
25
27
|
},
|
|
26
28
|
stopReason: "error",
|
|
27
29
|
timestamp: Date.now(),
|
|
28
|
-
errorMessage: `
|
|
30
|
+
errorMessage: `Cursor provider runtime failed: ${sanitizeCursorProviderError(error, apiKey)}`,
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -37,12 +39,11 @@ export function streamCursorLazy(
|
|
|
37
39
|
const outer = createAssistantMessageEventStream();
|
|
38
40
|
queueMicrotask(async () => {
|
|
39
41
|
try {
|
|
40
|
-
const { streamCursor } = await import("./cursor-provider.js");
|
|
41
42
|
for await (const event of streamCursor(model, context, options)) {
|
|
42
43
|
outer.push(event);
|
|
43
44
|
}
|
|
44
45
|
} catch (error) {
|
|
45
|
-
const message =
|
|
46
|
+
const message = makeProviderRuntimeErrorMessage(model, error, options?.apiKey);
|
|
46
47
|
outer.push({ type: "error", reason: "error", error: message });
|
|
47
48
|
outer.end(message);
|
|
48
49
|
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { type CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
|
|
22
22
|
import { resetSessionCursorAgent } from "./cursor-session-agent.js";
|
|
23
23
|
import { applyCursorUsage } from "./cursor-usage-accounting.js";
|
|
24
|
-
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
24
|
+
import { CURSOR_TEXT_MESSAGE_SEPARATOR, CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
25
25
|
import { emitDisplayOnlyTraceBlock } from "./cursor-display-only-trace.js";
|
|
26
26
|
import { trimCurrentTurnAlreadyEmittedCursorText } from "./cursor-run-final-text.js";
|
|
27
27
|
import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
|
|
@@ -88,11 +88,9 @@ function splitTextIntoReplayDeltas(text: string): string[] {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
async function emitTextDeltas(
|
|
91
|
-
|
|
92
|
-
partial: AssistantMessage,
|
|
91
|
+
emitter: CursorPartialContentEmitter,
|
|
93
92
|
deltas: string[],
|
|
94
93
|
): Promise<string> {
|
|
95
|
-
const emitter = new CursorPartialContentEmitter(stream, partial, -1, true);
|
|
96
94
|
for (const delta of deltas) {
|
|
97
95
|
emitter.appendTextDelta(delta);
|
|
98
96
|
await Promise.resolve();
|
|
@@ -121,11 +119,11 @@ export function flushPendingCursorLiveRunTraceEventsToStream(
|
|
|
121
119
|
stream: AssistantMessageEventStream,
|
|
122
120
|
partial: AssistantMessage,
|
|
123
121
|
run: CursorLiveRun,
|
|
124
|
-
options?: { includeTracesBehindQueuedTools?: boolean },
|
|
122
|
+
options?: { includeTracesBehindQueuedTools?: boolean; emitter?: CursorPartialContentEmitter },
|
|
125
123
|
): void {
|
|
126
124
|
if (run.disposed) return;
|
|
127
125
|
const turn: CursorLiveTurnState = {
|
|
128
|
-
emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
126
|
+
emitter: options?.emitter ?? new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
129
127
|
emittedText: "",
|
|
130
128
|
};
|
|
131
129
|
while (true) {
|
|
@@ -157,6 +155,12 @@ function emitCursorLiveQueuedEvent(
|
|
|
157
155
|
turn.emitter.appendThinkingDelta(event.text);
|
|
158
156
|
} else if (event.type === "thinking-completed") {
|
|
159
157
|
turn.emitter.closeThinking();
|
|
158
|
+
} else if (event.type === "text-completed") {
|
|
159
|
+
turn.emitter.completeTextMessage();
|
|
160
|
+
// Logical separators also keep suffix/prefix dedup aware of SDK messages,
|
|
161
|
+
// including boundaries queued across Pi tool-use turns.
|
|
162
|
+
if (turn.emittedText) turn.emittedText += CURSOR_TEXT_MESSAGE_SEPARATOR;
|
|
163
|
+
if (run?.emittedText) run.emittedText += CURSOR_TEXT_MESSAGE_SEPARATOR;
|
|
160
164
|
} else if (event.type === "text-delta") {
|
|
161
165
|
turn.emittedText += event.text;
|
|
162
166
|
if (run) run.emittedText += event.text;
|
|
@@ -296,7 +300,7 @@ export async function drainCursorLiveRunTurn(
|
|
|
296
300
|
context: Context,
|
|
297
301
|
run: CursorLiveRun,
|
|
298
302
|
toolResultInputTokens: number,
|
|
299
|
-
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
|
|
303
|
+
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder; emitter?: CursorPartialContentEmitter },
|
|
300
304
|
): Promise<CursorLiveRunDrainOutcome> {
|
|
301
305
|
const debugRecorder = options.debugRecorder ?? run.debugRecorder;
|
|
302
306
|
debugRecorder?.recordDrainEvent("turn_start", {
|
|
@@ -308,7 +312,7 @@ export async function drainCursorLiveRunTurn(
|
|
|
308
312
|
let outcome: CursorLiveRunDrainOutcome | undefined;
|
|
309
313
|
let outcomeDetails: Record<string, unknown> = {};
|
|
310
314
|
const turn: CursorLiveTurnState = {
|
|
311
|
-
emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
315
|
+
emitter: options.emitter ?? new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
312
316
|
emittedText: "",
|
|
313
317
|
};
|
|
314
318
|
|
|
@@ -378,11 +382,12 @@ export async function drainCursorLiveRunTurn(
|
|
|
378
382
|
turn.emitter.closeAll();
|
|
379
383
|
const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
|
|
380
384
|
if (finalText) {
|
|
381
|
-
await emitTextDeltas(
|
|
385
|
+
await emitTextDeltas(turn.emitter, splitTextIntoReplayDeltas(finalText));
|
|
382
386
|
}
|
|
383
387
|
applyCursorUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens), {
|
|
384
388
|
runtime: "local",
|
|
385
389
|
turn: cursorLiveRuns.takeSdkTurnUsage(run),
|
|
390
|
+
billed: run.billedTurnUsage,
|
|
386
391
|
});
|
|
387
392
|
if (run.resumeNotice) {
|
|
388
393
|
emitDisplayOnlyTraceBlock(stream, partial, run.resumeNotice);
|
|
@@ -186,6 +186,7 @@ export class CursorRunFinalizer {
|
|
|
186
186
|
applyCursorUsage(partial, model, context, prepared.meta.promptInputTokens, {
|
|
187
187
|
runtime: prepared.runtimeTarget,
|
|
188
188
|
turn: prepared.runtime.turnCoordinator.lastSdkTurnUsage,
|
|
189
|
+
billed: prepared.runtime.billedTurnUsage,
|
|
189
190
|
});
|
|
190
191
|
if (prepared.meta.resumeNotice) emitDisplayOnlyTraceBlock(stream, partial, prepared.meta.resumeNotice);
|
|
191
192
|
if (displayOnlyTraceBlock) emitDisplayOnlyTraceBlock(stream, partial, displayOnlyTraceBlock);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage, AssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { InteractionUpdate } from "@cursor/sdk";
|
|
3
|
+
import { CURSOR_TEXT_MESSAGE_SEPARATOR } from "./cursor-partial-content-emitter.js";
|
|
3
4
|
import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
4
5
|
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
5
6
|
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
@@ -65,6 +66,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
65
66
|
private readonly lifecycleEmitter: CursorToolLifecycleEmitter;
|
|
66
67
|
private readonly contentEmitter;
|
|
67
68
|
private sdkTurnUsage?: CursorSdkTurnUsage;
|
|
69
|
+
private hasPendingTextMessage = false;
|
|
68
70
|
|
|
69
71
|
constructor(options: CursorSdkTurnCoordinatorOptions) {
|
|
70
72
|
this.stream = options.stream;
|
|
@@ -153,6 +155,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
153
155
|
cursorLiveRuns.recordSdkTurnEnded(this.liveRun, sdkTurnUsage);
|
|
154
156
|
}
|
|
155
157
|
if (update.type === "text-delta") {
|
|
158
|
+
if (update.text) this.hasPendingTextMessage = true;
|
|
156
159
|
this.textDeltas.push(update.text);
|
|
157
160
|
if (this.liveRun) {
|
|
158
161
|
cursorLiveRuns.queueEvent(this.liveRun, { type: "text-delta", text: update.text });
|
|
@@ -247,6 +250,18 @@ export class CursorSdkTurnCoordinator {
|
|
|
247
250
|
|
|
248
251
|
handleStep(stepEnvelope: unknown): void {
|
|
249
252
|
const stepType = getField(stepEnvelope, "type");
|
|
253
|
+
if (stepType === "assistantMessage") {
|
|
254
|
+
// SDK onStep completes a message; onDelta chunks within it are tokens,
|
|
255
|
+
// not separate messages. Keep the boundary in both output and final-text
|
|
256
|
+
// deduplication, without replaying the completed step's text a second time.
|
|
257
|
+
if (this.hasPendingTextMessage) {
|
|
258
|
+
this.hasPendingTextMessage = false;
|
|
259
|
+
this.textDeltas.push(CURSOR_TEXT_MESSAGE_SEPARATOR);
|
|
260
|
+
if (this.liveRun) cursorLiveRuns.queueEvent(this.liveRun, { type: "text-completed" });
|
|
261
|
+
else this.contentEmitter.completeTextMessage();
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
250
265
|
const step = getField(stepEnvelope, "message") ? stepEnvelope : undefined;
|
|
251
266
|
const rawStepToolCall = getField(step, "message");
|
|
252
267
|
if (stepType !== "toolCall") return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
|
|
2
|
+
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
2
3
|
import {
|
|
3
4
|
cursorLiveRuns,
|
|
4
5
|
drainCursorLiveRunTurn,
|
|
@@ -28,6 +29,9 @@ export async function emitCursorLiveTurn(emitParams: EmitCursorLiveTurnParams):
|
|
|
28
29
|
const { liveRun, turnCoordinator } = prepared.runtime;
|
|
29
30
|
|
|
30
31
|
const { options, model } = params;
|
|
32
|
+
// Drain and abort recovery write to the same partial. Keep its open text block
|
|
33
|
+
// and deferred message boundary together; a new Pi turn still gets a new emitter.
|
|
34
|
+
const emitter = new CursorPartialContentEmitter(params.stream, params.partial, -1, true);
|
|
31
35
|
try {
|
|
32
36
|
await cursorLiveRuns.withRunLease(liveRun, options?.signal, async () => {
|
|
33
37
|
await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
|
|
@@ -35,6 +39,7 @@ export async function emitCursorLiveTurn(emitParams: EmitCursorLiveTurnParams):
|
|
|
35
39
|
turnCoordinator.closeTraceBlock();
|
|
36
40
|
await drainCursorLiveRunTurn(params.stream, params.partial, model, params.context, liveRun, 0, {
|
|
37
41
|
mode: "emit",
|
|
42
|
+
emitter,
|
|
38
43
|
signal: options?.signal,
|
|
39
44
|
debugRecorder: sdkEventDebug,
|
|
40
45
|
});
|
|
@@ -45,6 +50,7 @@ export async function emitCursorLiveTurn(emitParams: EmitCursorLiveTurnParams):
|
|
|
45
50
|
turnCoordinator.closeTraceBlock();
|
|
46
51
|
flushPendingCursorLiveRunTraceEventsToStream(params.stream, params.partial, liveRun, {
|
|
47
52
|
includeTracesBehindQueuedTools: true,
|
|
53
|
+
emitter,
|
|
48
54
|
});
|
|
49
55
|
}
|
|
50
56
|
throw caught;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./cursor-provider-run-outcome.js";
|
|
18
18
|
import type { CursorProviderTurnPrepareResult } from "./cursor-provider-turn-types.js";
|
|
19
19
|
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
20
|
+
import { attachCursorSdkBilledTurnUsage } from "./cursor-sdk-billed-usage.js";
|
|
20
21
|
|
|
21
22
|
export async function cacheSdkContextWindow(
|
|
22
23
|
agentId: string,
|
|
@@ -149,6 +150,16 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
|
|
|
149
150
|
resolvedApiKey: params.resolvedApiKey,
|
|
150
151
|
optionsApiKey: params.optionsApiKey,
|
|
151
152
|
});
|
|
153
|
+
const billed = await attachCursorSdkBilledTurnUsage({
|
|
154
|
+
agent: params.prepared.agent,
|
|
155
|
+
agentId: params.run.agentId,
|
|
156
|
+
runtime: params.prepared.runtimeTarget,
|
|
157
|
+
runId: params.run.id,
|
|
158
|
+
});
|
|
159
|
+
params.prepared.runtime.billedTurnUsage = billed.turn;
|
|
160
|
+
if (params.prepared.runtime.liveRun) {
|
|
161
|
+
params.prepared.runtime.liveRun.billedTurnUsage = billed.turn;
|
|
162
|
+
}
|
|
152
163
|
let displayOnlyTraceBlock: string | undefined;
|
|
153
164
|
if (params.prepared.runtimeTarget === "cloud" && isCursorRunFinishedSuccessfully(outcome)) {
|
|
154
165
|
let report: CursorCloudRunReport = { agentId: params.run.agentId, runId: params.run.id, branches: [] };
|
|
@@ -158,6 +169,7 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
|
|
|
158
169
|
run: params.run,
|
|
159
170
|
waitResult,
|
|
160
171
|
apiKey,
|
|
172
|
+
agentUsage: billed.agentUsage,
|
|
161
173
|
});
|
|
162
174
|
} catch (error) {
|
|
163
175
|
recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { getCursorConversationMessages, resolveCursorPiContext } from "./cursor-pi-context.js";
|
|
2
3
|
import type { AgentModeOption, ModelSelection, SDKAgent } from "@cursor/sdk";
|
|
3
4
|
import { configureCursorSdkHttp1 } from "./cursor-http1.js";
|
|
4
5
|
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
@@ -80,11 +81,15 @@ interface PrepareCursorProviderTurnContext extends PrepareCursorProviderTurnPara
|
|
|
80
81
|
|
|
81
82
|
function buildCursorCloudPromptContext(context: Context, handoff: "fresh" | "bootstrap" | "never"): Context {
|
|
82
83
|
if (handoff === "bootstrap") return context;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// Fresh cloud runs omit history, not the current Pi instructions. Replay before
|
|
85
|
+
// selecting the user message, otherwise transcript-only inputs lose the prompt.
|
|
86
|
+
const current = resolveCursorPiContext(context);
|
|
87
|
+
const messages = getCursorConversationMessages(context);
|
|
88
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
89
|
+
const message = messages[index];
|
|
90
|
+
if (message.role === "user") return { ...current, messages: [message] };
|
|
86
91
|
}
|
|
87
|
-
return { ...
|
|
92
|
+
return { ...current, messages: messages.slice(-1) };
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
const CLOUD_SEND_PLAN: CursorSessionSendPlan = { mode: "bootstrap", resetAgent: false, reason: "initial" };
|
|
@@ -135,7 +140,7 @@ async function prepareCursorCloudProviderTurn(
|
|
|
135
140
|
repo: resolvedConfig.cloud.repo.value,
|
|
136
141
|
branch: resolvedConfig.cloud.branch.value,
|
|
137
142
|
}),
|
|
138
|
-
hasPriorContext: context.
|
|
143
|
+
hasPriorContext: getCursorConversationMessages(context).length > 1,
|
|
139
144
|
});
|
|
140
145
|
if (!preflight.ok) throw new Error(formatCursorCloudPreflightError(preflight));
|
|
141
146
|
if (getPendingCursorLiveRun(context) || getActiveCursorLiveRunForCurrentScope()) {
|
|
@@ -14,6 +14,7 @@ import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
|
14
14
|
import type { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
|
|
15
15
|
import type { CursorPrompt } from "./context.js";
|
|
16
16
|
import type { CursorResolvedSetting } from "./cursor-config.js";
|
|
17
|
+
import type { CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
|
|
17
18
|
|
|
18
19
|
export interface CursorProviderTurnRunnerParams {
|
|
19
20
|
model: Model<Api>;
|
|
@@ -44,6 +45,7 @@ export interface CursorProviderTurnSendMeta {
|
|
|
44
45
|
|
|
45
46
|
interface CursorProviderTurnRuntimeBase {
|
|
46
47
|
turnCoordinator: CursorSdkTurnCoordinator;
|
|
48
|
+
billedTurnUsage?: CursorSdkTurnUsage;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { SDKAgent } from "@cursor/sdk";
|
|
2
|
+
import type { CursorRuntime } from "./cursor-config.js";
|
|
3
|
+
import { asRecord, getArray, getString } from "./cursor-record-utils.js";
|
|
4
|
+
import { readCursorSdkTurnUsage, type CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
|
|
5
|
+
|
|
6
|
+
const BILLED_USAGE_TIMEOUT_MS = 5000;
|
|
7
|
+
|
|
8
|
+
// ponytail: process-lifetime watermark of billed usage UUIDs per agentId; reset on process exit.
|
|
9
|
+
// Upgrade to session-scoped storage if multi-day processes retain enough agent IDs to matter.
|
|
10
|
+
const seenBilledRunIdsByAgent = new Map<string, Set<string>>();
|
|
11
|
+
|
|
12
|
+
export function isCursorSdkClientMintedRunId(runId: string): boolean {
|
|
13
|
+
return runId.startsWith("run-");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sumCursorSdkTurnUsage(usages: readonly CursorSdkTurnUsage[]): CursorSdkTurnUsage | undefined {
|
|
17
|
+
if (usages.length === 0) return undefined;
|
|
18
|
+
return usages.reduce(
|
|
19
|
+
(total, usage) => ({
|
|
20
|
+
inputTokens: total.inputTokens + usage.inputTokens,
|
|
21
|
+
outputTokens: total.outputTokens + usage.outputTokens,
|
|
22
|
+
cacheReadTokens: total.cacheReadTokens + usage.cacheReadTokens,
|
|
23
|
+
cacheWriteTokens: total.cacheWriteTokens + usage.cacheWriteTokens,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function peekCursorBilledUsageRunIds(agentId: string): ReadonlySet<string> {
|
|
29
|
+
return seenBilledRunIdsByAgent.get(agentId) ?? new Set();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function rememberCursorBilledUsageRunIds(agentId: string, runIds: readonly string[]): void {
|
|
33
|
+
if (runIds.length === 0) return;
|
|
34
|
+
let seen = seenBilledRunIdsByAgent.get(agentId);
|
|
35
|
+
if (!seen) {
|
|
36
|
+
seen = new Set();
|
|
37
|
+
seenBilledRunIdsByAgent.set(agentId, seen);
|
|
38
|
+
}
|
|
39
|
+
for (const runId of runIds) seen.add(runId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs = BILLED_USAGE_TIMEOUT_MS): Promise<T | undefined> {
|
|
43
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
44
|
+
const timeout = new Promise<undefined>((resolve) => {
|
|
45
|
+
timer = setTimeout(() => resolve(undefined), timeoutMs);
|
|
46
|
+
timer.unref?.();
|
|
47
|
+
});
|
|
48
|
+
return Promise.race([promise.catch(() => undefined), timeout]).finally(() => {
|
|
49
|
+
if (timer) clearTimeout(timer);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function selectCursorBilledTurnUsage(
|
|
54
|
+
agentUsage: unknown,
|
|
55
|
+
options: { runtime: CursorRuntime; runId?: string; seenRunIds?: ReadonlySet<string> },
|
|
56
|
+
): { turn?: CursorSdkTurnUsage; runIds: string[] } {
|
|
57
|
+
const runs = (getArray(asRecord(agentUsage), "runs") ?? []).flatMap((item) => {
|
|
58
|
+
const record = asRecord(item);
|
|
59
|
+
const runId = getString(record, "runId");
|
|
60
|
+
const usage = readCursorSdkTurnUsage(record?.usage);
|
|
61
|
+
return runId && usage ? [{ runId, usage }] : [];
|
|
62
|
+
});
|
|
63
|
+
if (options.runtime === "cloud" && options.runId) {
|
|
64
|
+
const match = runs.find((run) => run.runId === options.runId);
|
|
65
|
+
return match ? { turn: match.usage, runIds: [match.runId] } : { runIds: [] };
|
|
66
|
+
}
|
|
67
|
+
const unseen = runs.filter((run) => !options.seenRunIds?.has(run.runId));
|
|
68
|
+
return { turn: sumCursorSdkTurnUsage(unseen.map((run) => run.usage)), runIds: unseen.map((run) => run.runId) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function fetchCursorSdkAgentUsage(
|
|
72
|
+
agent: SDKAgent,
|
|
73
|
+
options: { runtime: CursorRuntime; runId?: string },
|
|
74
|
+
): Promise<unknown | undefined> {
|
|
75
|
+
if (typeof agent.getUsage !== "function") return undefined;
|
|
76
|
+
const query =
|
|
77
|
+
options.runtime === "cloud" && options.runId && isCursorSdkClientMintedRunId(options.runId)
|
|
78
|
+
? { runId: options.runId }
|
|
79
|
+
: undefined;
|
|
80
|
+
return withTimeout(Promise.resolve(agent.getUsage(query)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function attachCursorSdkBilledTurnUsage(options: {
|
|
84
|
+
agent: SDKAgent;
|
|
85
|
+
agentId: string;
|
|
86
|
+
runtime: CursorRuntime;
|
|
87
|
+
runId?: string;
|
|
88
|
+
}): Promise<{ agentUsage?: unknown; turn?: CursorSdkTurnUsage }> {
|
|
89
|
+
const agentUsage = await fetchCursorSdkAgentUsage(options.agent, {
|
|
90
|
+
runtime: options.runtime,
|
|
91
|
+
runId: options.runId,
|
|
92
|
+
});
|
|
93
|
+
if (!agentUsage) return {};
|
|
94
|
+
const selected = selectCursorBilledTurnUsage(agentUsage, {
|
|
95
|
+
runtime: options.runtime,
|
|
96
|
+
runId: options.runtime === "cloud" ? options.runId : undefined,
|
|
97
|
+
seenRunIds: peekCursorBilledUsageRunIds(options.agentId),
|
|
98
|
+
});
|
|
99
|
+
rememberCursorBilledUsageRunIds(options.agentId, selected.runIds);
|
|
100
|
+
return { agentUsage, turn: selected.turn };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const __testUtils = {
|
|
104
|
+
reset(): void {
|
|
105
|
+
seenBilledRunIdsByAgent.clear();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -75,10 +75,12 @@ function isCursorSdkAbortError(error: unknown): boolean {
|
|
|
75
75
|
// The exact observed incident: the Cursor SDK 1.0.23 local shell executor writes a
|
|
76
76
|
// spawned child's stdin without a stream 'error' listener, so a child exiting while
|
|
77
77
|
// a write is in flight surfaces a raw `write EPIPE` uncaught exception whose stack
|
|
78
|
-
// is exactly the single async pipe-write completion frame.
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
78
|
+
// is exactly the single async pipe-write completion frame. Installed 1.0.27 attaches
|
|
79
|
+
// a no-op `error` listener before that write; keep this guard as defense in depth.
|
|
80
|
+
// Pi's own piped-stdout or dead-terminal EPIPE normally surfaces through the
|
|
81
|
+
// synchronous write-dispatch path with multiple frames (afterWriteDispatched /
|
|
82
|
+
// Socket._writeGeneric) and must stay fatal per Unix convention, so anything beyond
|
|
83
|
+
// this one-frame contract is rejected.
|
|
82
84
|
const OBSERVED_CLOSED_PIPE_STACK_FRAME =
|
|
83
85
|
/^\s+at WriteWrap\.onWriteComplete \[as oncomplete\] \(node:internal\/stream_base_commons:\d+:\d+\)$/;
|
|
84
86
|
|