pi-cursor-sdk 0.1.14 → 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.
@@ -6,14 +6,39 @@ import {
6
6
  type Model,
7
7
  type SimpleStreamOptions,
8
8
  type AssistantMessage,
9
+ type ToolResultMessage,
9
10
  } from "@earendil-works/pi-ai";
11
+ import { AsyncLocalStorage } from "node:async_hooks";
10
12
  import { Agent, createAgentPlatform } from "@cursor/sdk";
11
13
  import type { InteractionUpdate, SDKAgent, SettingSource } from "@cursor/sdk";
12
- import { buildCursorPrompt, type CursorPrompt } from "./context.js";
14
+ import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
15
+ import { buildCursorSendPrompt } from "./context.js";
16
+ import {
17
+ acquireSessionCursorAgent,
18
+ commitSessionAgentSend,
19
+ disposeAllSessionCursorAgents,
20
+ resetSessionCursorAgent,
21
+ } from "./cursor-session-agent.js";
22
+ import {
23
+ type CursorPiBridgeToolRequest,
24
+ type CursorPiToolBridgeRun,
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";
37
+ import { getCursorSessionCwd } from "./cursor-session-cwd.js";
13
38
  import { getEffectiveFastForModelId } from "./cursor-state.js";
14
39
  import { buildCursorModelSelection } from "./model-discovery.js";
15
40
  import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
16
- import { buildCursorPiToolDisplay, formatCursorToolTranscript, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
41
+ import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
17
42
  import {
18
43
  canRenderCursorToolNatively,
19
44
  isCursorNativeToolDisplayRuntimeEnabled,
@@ -56,26 +81,34 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
56
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.";
57
82
  const AUTH_CURSOR_SDK_ERROR_MESSAGE =
58
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.";
59
- const APPROX_CHARS_PER_TOKEN = 4;
60
- const IMAGE_TOKEN_ESTIMATE = 1200;
61
84
  const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
62
85
  const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
63
86
  const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
64
87
  const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
88
+ const cursorSdkOutputSuppression = new AsyncLocalStorage<boolean>();
65
89
 
66
- type CursorNativeQueuedEvent =
90
+ type CursorLiveQueuedEvent =
67
91
  | { type: "thinking-delta"; text: string }
68
92
  | { type: "thinking-completed" }
69
93
  | { type: "text-delta"; text: string }
70
- | { type: "tool"; tool: CursorNativeToolDisplayItem };
94
+ | { type: "tool"; tool: CursorNativeToolDisplayItem }
95
+ | { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
96
+
97
+ interface CursorLiveSdkRun {
98
+ cancel(): Promise<void>;
99
+ }
71
100
 
72
- interface CursorNativeLiveRun {
101
+ interface CursorLiveRun {
73
102
  id: string;
74
103
  agent: SDKAgent;
75
- promptInputTokens: number;
76
- promptInputTokensReported: boolean;
77
- pendingEvents: CursorNativeQueuedEvent[];
104
+ bridgeRun?: CursorPiToolBridgeRun;
105
+ sessionBridgeRun?: CursorPiToolBridgeRun;
106
+ sessionAgentScopeKey?: string;
107
+ sdkRun?: CursorLiveSdkRun;
108
+ accounting: CursorLiveRunAccountingState;
109
+ pendingEvents: CursorLiveQueuedEvent[];
78
110
  textDeltas: string[];
111
+ emittedText: string;
79
112
  recordedToolDisplayIds: string[];
80
113
  finalText?: string;
81
114
  done: boolean;
@@ -86,16 +119,17 @@ interface CursorNativeLiveRun {
86
119
  waiters: Set<() => void>;
87
120
  }
88
121
 
89
- interface CursorNativeTurnState {
122
+ interface CursorLiveTurnState {
90
123
  stream: AssistantMessageEventStream;
91
124
  partial: AssistantMessage;
92
125
  thinkingContentIndex: number;
93
126
  textContentIndex: number;
127
+ emittedText: string;
94
128
  }
95
129
 
96
130
  let cursorNativeReplayCounter = 0;
97
131
  let cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
98
- const pendingCursorNativeRuns = new Map<string, CursorNativeLiveRun>();
132
+ const pendingCursorLiveRuns = new Map<string, CursorLiveRun>();
99
133
 
100
134
  function escapeRegExp(value: string): string {
101
135
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -134,7 +168,7 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
134
168
 
135
169
  function resolveCursorSettingSources(): SettingSource[] | undefined {
136
170
  const raw = process.env[CURSOR_SETTING_SOURCES_ENV]?.trim();
137
- if (!raw) return undefined;
171
+ if (!raw) return ["all"];
138
172
  const normalized = raw.toLowerCase();
139
173
  if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
140
174
  if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
@@ -153,6 +187,78 @@ function sanitizeError(error: unknown, apiKey?: string): string {
153
187
  return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
154
188
  }
155
189
 
190
+ function isCursorSdkOutputSuppressed(): boolean {
191
+ return cursorSdkOutputSuppression.getStore() === true;
192
+ }
193
+
194
+ function suppressCursorSdkOutput<T>(operation: () => Promise<T>): Promise<T> {
195
+ return cursorSdkOutputSuppression.run(true, operation);
196
+ }
197
+
198
+ const CURSOR_SDK_STARTUP_NOISE_PATTERNS = [
199
+ "managed_skills.",
200
+ "CursorPluginsAgentSkillsService load completed",
201
+ "LocalCursorRulesService load completed",
202
+ "AgentSkillsCursorRulesService load completed",
203
+ ];
204
+
205
+ function isCursorSdkStartupNoise(text: string): boolean {
206
+ return CURSOR_SDK_STARTUP_NOISE_PATTERNS.some((pattern) => text.includes(pattern));
207
+ }
208
+
209
+ function createFilteredProcessWrite<TWrite extends typeof process.stdout.write>(write: TWrite, stream: NodeJS.WriteStream): TWrite {
210
+ return ((
211
+ chunk: string | Uint8Array,
212
+ encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
213
+ callback?: (error?: Error | null) => void,
214
+ ): boolean => {
215
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
216
+ if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) {
217
+ const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
218
+ done?.();
219
+ return true;
220
+ }
221
+ return write.call(stream, chunk as string, encodingOrCallback as BufferEncoding, callback);
222
+ }) as TWrite;
223
+ }
224
+
225
+ function createFilteredConsoleMethod<TMethod extends typeof console.log>(method: TMethod): TMethod {
226
+ return ((...args: Parameters<TMethod>): void => {
227
+ const text = args.map((arg) => (typeof arg === "string" ? arg : String(arg))).join(" ");
228
+ if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) return;
229
+ method(...args);
230
+ }) as TMethod;
231
+ }
232
+
233
+ function installCursorSdkOutputFilter(): () => void {
234
+ const stdoutWrite = process.stdout.write;
235
+ const stderrWrite = process.stderr.write;
236
+ const consoleLog = console.log;
237
+ const consoleInfo = console.info;
238
+ const consoleWarn = console.warn;
239
+ const consoleError = console.error;
240
+ const consoleDebug = console.debug;
241
+ process.stdout.write = createFilteredProcessWrite(stdoutWrite, process.stdout);
242
+ process.stderr.write = createFilteredProcessWrite(stderrWrite, process.stderr) as typeof process.stderr.write;
243
+ console.log = createFilteredConsoleMethod(consoleLog);
244
+ console.info = createFilteredConsoleMethod(consoleInfo);
245
+ console.warn = createFilteredConsoleMethod(consoleWarn);
246
+ console.error = createFilteredConsoleMethod(consoleError);
247
+ console.debug = createFilteredConsoleMethod(consoleDebug);
248
+ let restored = false;
249
+ return () => {
250
+ if (restored) return;
251
+ restored = true;
252
+ process.stdout.write = stdoutWrite;
253
+ process.stderr.write = stderrWrite;
254
+ console.log = consoleLog;
255
+ console.info = consoleInfo;
256
+ console.warn = consoleWarn;
257
+ console.error = consoleError;
258
+ console.debug = consoleDebug;
259
+ };
260
+ }
261
+
156
262
  function getObjectField(value: unknown, field: string): unknown {
157
263
  if (!value || typeof value !== "object") return undefined;
158
264
  return (value as Record<string, unknown>)[field];
@@ -163,6 +269,7 @@ function getCursorToolName(toolCall: unknown): string {
163
269
  const data = toolCall as Record<string, unknown>;
164
270
  if (typeof data.name === "string") return data.name;
165
271
  if (typeof data.type === "string") return data.type;
272
+ if (typeof data.toolName === "string") return data.toolName;
166
273
  return "unknown";
167
274
  }
168
275
 
@@ -177,27 +284,6 @@ async function cacheSdkContextWindow(agentId: string, modelId: string): Promise<
177
284
  }
178
285
  }
179
286
 
180
- function estimateTextTokens(text: string): number {
181
- return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
182
- }
183
-
184
- function estimatePromptInputTokens(prompt: CursorPrompt): number {
185
- return estimateTextTokens(prompt.text) + prompt.images.length * IMAGE_TOKEN_ESTIMATE;
186
- }
187
-
188
- function getPromptInputTokenBudget(model: Model<Api>): number {
189
- const outputReserveTokens = Math.min(model.maxTokens, Math.max(1, Math.floor(model.contextWindow * 0.2)));
190
- return Math.max(1, model.contextWindow - outputReserveTokens);
191
- }
192
-
193
- function setApproximateUsage(partial: AssistantMessage, promptInputTokens: number, outputText: string): void {
194
- partial.usage.input = promptInputTokens;
195
- partial.usage.output = estimateTextTokens(outputText);
196
- partial.usage.cacheRead = 0;
197
- partial.usage.cacheWrite = 0;
198
- partial.usage.totalTokens = partial.usage.input + partial.usage.output;
199
- }
200
-
201
287
  function sanitizeSingleLine(value: string): string {
202
288
  return value.replace(/\s+/g, " ").trim();
203
289
  }
@@ -215,6 +301,63 @@ function hasUsableText(value: string | undefined): value is string {
215
301
  return typeof value === "string" && value.trim().length > 0;
216
302
  }
217
303
 
304
+ interface CursorShellOutputDelta {
305
+ stream: "stdout" | "stderr";
306
+ data: string;
307
+ }
308
+
309
+ interface CursorShellOutputDeltas {
310
+ stdout: string[];
311
+ stderr: string[];
312
+ }
313
+
314
+ function isCursorShellToolCall(toolCall: unknown): boolean {
315
+ const normalizedName = getCursorToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase();
316
+ return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
317
+ }
318
+
319
+ function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
320
+ if (update.type !== "shell-output-delta") return undefined;
321
+ const event = getObjectField(update, "event");
322
+ const eventCase = getObjectField(event, "case");
323
+ if (eventCase !== "stdout" && eventCase !== "stderr") return undefined;
324
+ const value = getObjectField(event, "value");
325
+ const data = getObjectField(value, "data");
326
+ if (typeof data !== "string" || data.length === 0) return undefined;
327
+ return { stream: eventCase, data };
328
+ }
329
+
330
+ function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: CursorShellOutputDeltas | undefined): unknown {
331
+ if (!deltas) return toolCall;
332
+ const stdout = deltas.stdout.join("");
333
+ const stderr = deltas.stderr.join("");
334
+ if (!hasUsableText(stdout) && !hasUsableText(stderr)) return toolCall;
335
+
336
+ const toolRecord = toolCall && typeof toolCall === "object" && !Array.isArray(toolCall) ? (toolCall as Record<string, unknown>) : undefined;
337
+ const result = getObjectField(toolRecord, "result");
338
+ const resultRecord = result && typeof result === "object" && !Array.isArray(result) ? (result as Record<string, unknown>) : undefined;
339
+ if (!toolRecord || !resultRecord || resultRecord.status !== "success") return toolCall;
340
+
341
+ const value = getObjectField(resultRecord, "value");
342
+ const valueRecord = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
343
+ const completedStdout = getObjectField(valueRecord, "stdout");
344
+ const completedStderr = getObjectField(valueRecord, "stderr");
345
+ if (hasUsableText(typeof completedStdout === "string" ? completedStdout : undefined)) return toolCall;
346
+ if (hasUsableText(typeof completedStderr === "string" ? completedStderr : undefined)) return toolCall;
347
+
348
+ return {
349
+ ...toolRecord,
350
+ result: {
351
+ ...resultRecord,
352
+ value: {
353
+ ...(valueRecord ?? {}),
354
+ stdout,
355
+ stderr,
356
+ },
357
+ },
358
+ };
359
+ }
360
+
218
361
  function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
219
362
  if (typeof value === "string") return scrubSensitiveText(value, apiKey);
220
363
  if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
@@ -233,16 +376,34 @@ function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | und
233
376
  return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
234
377
  }
235
378
 
236
- function getPendingCursorNativeReplayId(context: Context): string | undefined {
379
+ function getPendingCursorLiveRun(context: Context): CursorLiveRun | undefined {
237
380
  for (let index = context.messages.length - 1; index >= 0; index -= 1) {
238
381
  const message = context.messages[index];
239
382
  if (message.role !== "toolResult") break;
240
383
  const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
241
- if (replayId && pendingCursorNativeRuns.has(replayId)) return replayId;
384
+ if (replayId) {
385
+ const replayRun = pendingCursorLiveRuns.get(replayId);
386
+ if (replayRun) return replayRun;
387
+ }
388
+ for (const run of pendingCursorLiveRuns.values()) {
389
+ if (run.bridgeRun?.hasPendingPiToolCallId(message.toolCallId)) return run;
390
+ }
242
391
  }
243
392
  return undefined;
244
393
  }
245
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
+
246
407
  function splitTextIntoReplayDeltas(text: string): string[] {
247
408
  const deltas: string[] = [];
248
409
  let remaining = text;
@@ -278,36 +439,37 @@ async function emitTextDeltas(
278
439
  return block.text;
279
440
  }
280
441
 
281
- function notifyCursorNativeRun(run: CursorNativeLiveRun): void {
442
+ function notifyCursorNativeRun(run: CursorLiveRun): void {
282
443
  for (const waiter of run.waiters) waiter();
283
444
  run.waiters.clear();
284
445
  }
285
446
 
286
- function queueCursorNativeEvent(run: CursorNativeLiveRun, event: CursorNativeQueuedEvent): void {
447
+ function queueCursorNativeEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void {
287
448
  run.pendingEvents.push(event);
288
449
  notifyCursorNativeRun(run);
289
450
  }
290
451
 
291
- function clearCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
452
+ function clearCursorNativeRunIdleDispose(run: CursorLiveRun): void {
292
453
  if (!run.idleDisposeTimer) return;
293
454
  clearTimeout(run.idleDisposeTimer);
294
455
  run.idleDisposeTimer = undefined;
295
456
  }
296
457
 
297
- function scheduleCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
458
+ function scheduleCursorNativeRunIdleDispose(run: CursorLiveRun): void {
298
459
  if (run.disposed) return;
299
460
  clearCursorNativeRunIdleDispose(run);
300
461
  run.idleDisposeTimer = setTimeout(() => {
301
- void disposeCursorNativeRun(run);
462
+ void releaseCursorLiveRun(run);
302
463
  }, cursorNativeReplayIdleDisposeMs);
303
464
  run.idleDisposeTimer.unref?.();
304
465
  }
305
466
 
306
- function isCursorNativeRunReady(run: CursorNativeLiveRun): boolean {
467
+ function isCursorNativeRunReady(run: CursorLiveRun): boolean {
307
468
  return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
308
469
  }
309
470
 
310
- async function waitForCursorNativeRunProgress(run: CursorNativeLiveRun, signal?: AbortSignal): Promise<void> {
471
+ async function waitForCursorNativeRunProgress(run: CursorLiveRun, signal?: AbortSignal): Promise<void> {
472
+ if (signal?.aborted) throw new CursorAbortError();
311
473
  if (isCursorNativeRunReady(run)) return;
312
474
  await new Promise<void>((resolve, reject) => {
313
475
  let waiter: (() => void) | undefined;
@@ -324,16 +486,21 @@ async function waitForCursorNativeRunProgress(run: CursorNativeLiveRun, signal?:
324
486
  resolve();
325
487
  };
326
488
  run.waiters.add(waiter);
489
+ if (signal?.aborted) {
490
+ onAbort();
491
+ return;
492
+ }
327
493
  signal?.addEventListener("abort", onAbort, { once: true });
328
494
  });
329
495
  }
330
496
 
331
- async function settleCursorNativeToolBatch(run: CursorNativeLiveRun): Promise<void> {
332
- if (run.pendingEvents[0]?.type !== "tool") return;
497
+ async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<void> {
498
+ const eventType = run.pendingEvents[0]?.type;
499
+ if (eventType !== "tool" && eventType !== "bridge-tool") return;
333
500
  await new Promise((resolve) => setTimeout(resolve, 75));
334
501
  }
335
502
 
336
- function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
503
+ function closeCursorNativeThinkingBlock(turn: CursorLiveTurnState): void {
337
504
  if (turn.thinkingContentIndex < 0) return;
338
505
  const block = turn.partial.content[turn.thinkingContentIndex];
339
506
  if (block.type === "thinking") {
@@ -347,7 +514,7 @@ function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
347
514
  turn.thinkingContentIndex = -1;
348
515
  }
349
516
 
350
- function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
517
+ function closeCursorNativeTextBlock(turn: CursorLiveTurnState): string {
351
518
  if (turn.textContentIndex < 0) return "";
352
519
  const contentIndex = turn.textContentIndex;
353
520
  const block = turn.partial.content[contentIndex];
@@ -362,12 +529,12 @@ function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
362
529
  return block.text;
363
530
  }
364
531
 
365
- function closeCursorNativeTurnBlocks(turn: CursorNativeTurnState): string {
532
+ function closeCursorNativeTurnBlocks(turn: CursorLiveTurnState): string {
366
533
  closeCursorNativeThinkingBlock(turn);
367
534
  return closeCursorNativeTextBlock(turn);
368
535
  }
369
536
 
370
- function emitCursorNativeThinkingDelta(turn: CursorNativeTurnState, delta: string): void {
537
+ function emitCursorNativeThinkingDelta(turn: CursorLiveTurnState, delta: string): void {
371
538
  closeCursorNativeTextBlock(turn);
372
539
  if (turn.thinkingContentIndex < 0) {
373
540
  turn.thinkingContentIndex = turn.partial.content.length;
@@ -380,7 +547,7 @@ function emitCursorNativeThinkingDelta(turn: CursorNativeTurnState, delta: strin
380
547
  turn.stream.push({ type: "thinking_delta", contentIndex: turn.thinkingContentIndex, delta, partial: turn.partial });
381
548
  }
382
549
 
383
- function emitCursorNativeTextDelta(turn: CursorNativeTurnState, delta: string): void {
550
+ function emitCursorNativeTextDelta(turn: CursorLiveTurnState, delta: string): void {
384
551
  closeCursorNativeThinkingBlock(turn);
385
552
  if (turn.textContentIndex < 0) {
386
553
  turn.textContentIndex = turn.partial.content.length;
@@ -393,20 +560,23 @@ function emitCursorNativeTextDelta(turn: CursorNativeTurnState, delta: string):
393
560
  turn.stream.push({ type: "text_delta", contentIndex: turn.textContentIndex, delta, partial: turn.partial });
394
561
  }
395
562
 
396
- function emitCursorNativeQueuedEvent(
397
- turn: CursorNativeTurnState,
398
- event: Exclude<CursorNativeQueuedEvent, { type: "tool" }>,
563
+ function emitCursorLiveQueuedEvent(
564
+ turn: CursorLiveTurnState,
565
+ event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
566
+ run?: CursorLiveRun,
399
567
  ): void {
400
568
  if (event.type === "thinking-delta") {
401
569
  emitCursorNativeThinkingDelta(turn, event.text);
402
570
  } else if (event.type === "thinking-completed") {
403
571
  closeCursorNativeThinkingBlock(turn);
404
572
  } else if (event.type === "text-delta") {
573
+ turn.emittedText += event.text;
574
+ if (run) run.emittedText += event.text;
405
575
  emitCursorNativeTextDelta(turn, event.text);
406
576
  }
407
577
  }
408
578
 
409
- function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToolDisplayItem[] {
579
+ function collectCursorNativeToolBatch(run: CursorLiveRun): CursorNativeToolDisplayItem[] {
410
580
  const tools: CursorNativeToolDisplayItem[] = [];
411
581
  while (run.pendingEvents[0]?.type === "tool") {
412
582
  const event = run.pendingEvents.shift();
@@ -415,19 +585,81 @@ function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToo
415
585
  return tools;
416
586
  }
417
587
 
418
- function takeCursorNativePromptInputTokens(run: CursorNativeLiveRun): number {
588
+ function collectCursorBridgeToolBatch(run: CursorLiveRun): CursorPiBridgeToolRequest[] {
589
+ const requests: CursorPiBridgeToolRequest[] = [];
590
+ while (run.pendingEvents[0]?.type === "bridge-tool") {
591
+ const event = run.pendingEvents.shift();
592
+ if (event?.type === "bridge-tool") requests.push(event.request);
593
+ }
594
+ return requests;
595
+ }
596
+
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 {
605
+ if (!text || !emittedText) return text;
606
+ if (text === emittedText) return "";
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
+ }
618
+ return text;
619
+ }
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
+
632
+ function selectCursorFinalText(
633
+ resultText: unknown,
634
+ textDeltas: readonly string[],
635
+ emittedText: string,
636
+ fallbackText?: string,
637
+ options?: { allowPartialPrefix?: boolean },
638
+ ): string {
639
+ const candidates = [typeof resultText === "string" ? resultText : undefined, fallbackText, textDeltas.join("")];
640
+ for (const candidate of candidates) {
641
+ if (!hasUsableText(candidate)) continue;
642
+ const trimmedCandidate = trimAlreadyEmittedCursorText(candidate, emittedText, options);
643
+ if (hasUsableText(trimmedCandidate)) return trimmedCandidate;
644
+ }
645
+ return "";
646
+ }
647
+
648
+ function takeCursorLiveSessionInputTokens(run: CursorLiveRun, toolResultInputTokens: number): number {
419
649
  // Native replay can split one Cursor run into multiple pi turns; count prompt input once.
420
- if (run.promptInputTokensReported) return 0;
421
- run.promptInputTokensReported = true;
422
- return run.promptInputTokens;
650
+ const taken = takeCursorLiveTurnInputTokens(run.accounting, toolResultInputTokens);
651
+ run.accounting = taken.state;
652
+ return taken.sessionInputTokens;
423
653
  }
424
654
 
425
655
  function emitCursorNativeToolUseTurn(
426
656
  stream: AssistantMessageEventStream,
427
657
  partial: AssistantMessage,
428
- run: CursorNativeLiveRun,
658
+ model: Model<Api>,
659
+ context: Context,
660
+ run: CursorLiveRun,
661
+ toolResultInputTokens: number,
429
662
  tools: CursorNativeToolDisplayItem[],
430
- outputText: string,
431
663
  ): void {
432
664
  const shouldTerminate = run.done && !run.finalText?.trim() && run.pendingEvents.length === 0;
433
665
  for (const tool of tools) {
@@ -446,76 +678,142 @@ function emitCursorNativeToolUseTurn(
446
678
  run.recordedToolDisplayIds.push(tool.id);
447
679
  }
448
680
  }
449
- setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
681
+ applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
682
+ partial.stopReason = "toolUse";
683
+ stream.push({ type: "done", reason: "toolUse", message: partial });
684
+ scheduleCursorNativeRunIdleDispose(run);
685
+ }
686
+
687
+ function emitCursorBridgeToolUseTurn(
688
+ stream: AssistantMessageEventStream,
689
+ partial: AssistantMessage,
690
+ model: Model<Api>,
691
+ context: Context,
692
+ run: CursorLiveRun,
693
+ toolResultInputTokens: number,
694
+ requests: CursorPiBridgeToolRequest[],
695
+ ): void {
696
+ for (const request of requests) {
697
+ const contentIndex = partial.content.length;
698
+ partial.content.push({
699
+ type: "toolCall",
700
+ id: request.piToolCallId,
701
+ name: request.piToolName,
702
+ arguments: request.args,
703
+ });
704
+ stream.push({ type: "toolcall_start", contentIndex, partial });
705
+ stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(request.args), partial });
706
+ const block = partial.content[contentIndex];
707
+ if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
708
+ }
709
+ applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
450
710
  partial.stopReason = "toolUse";
451
711
  stream.push({ type: "done", reason: "toolUse", message: partial });
452
712
  scheduleCursorNativeRunIdleDispose(run);
453
713
  }
454
714
 
455
- async function disposeCursorNativeRun(run: CursorNativeLiveRun): Promise<void> {
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> {
456
725
  if (run.disposed) return;
726
+ const abandoned = !isSuccessfulCursorLiveRun(run);
457
727
  run.disposed = true;
458
- pendingCursorNativeRuns.delete(run.id);
728
+ pendingCursorLiveRuns.delete(run.id);
459
729
  clearCursorNativeRunIdleDispose(run);
730
+ run.bridgeRun?.cancel("Cursor live run released");
460
731
  for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
461
732
  run.recordedToolDisplayIds = [];
462
733
  run.waiters.clear();
463
- try {
464
- await run.agent[Symbol.asyncDispose]();
465
- } catch {
466
- // disposal failure should not mask the provider result
734
+ if (run.sessionBridgeRun) {
735
+ run.sessionBridgeRun.setOnToolRequest(undefined);
736
+ }
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);
467
751
  }
468
752
  }
469
753
 
470
754
  async function emitCursorNativeRunNextTurn(
471
755
  stream: AssistantMessageEventStream,
472
756
  partial: AssistantMessage,
473
- run: CursorNativeLiveRun,
757
+ model: Model<Api>,
758
+ context: Context,
759
+ run: CursorLiveRun,
760
+ toolResultInputTokens: number,
474
761
  signal?: AbortSignal,
475
762
  ): Promise<void> {
476
- const turn: CursorNativeTurnState = {
763
+ const turn: CursorLiveTurnState = {
477
764
  stream,
478
765
  partial,
479
766
  thinkingContentIndex: -1,
480
767
  textContentIndex: -1,
768
+ emittedText: "",
481
769
  };
482
770
 
483
771
  while (true) {
484
772
  while (run.pendingEvents.length > 0) {
485
773
  const event = run.pendingEvents[0];
486
774
  if (event.type === "tool") {
487
- await settleCursorNativeToolBatch(run);
488
- const outputText = closeCursorNativeTurnBlocks(turn);
775
+ await settleCursorLiveToolBatch(run);
776
+ if (signal?.aborted) throw new CursorAbortError();
777
+ closeCursorNativeTurnBlocks(turn);
489
778
  const tools = collectCursorNativeToolBatch(run);
490
- emitCursorNativeToolUseTurn(stream, partial, run, tools, outputText);
779
+ emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, tools);
780
+ return;
781
+ }
782
+ if (event.type === "bridge-tool") {
783
+ await settleCursorLiveToolBatch(run);
784
+ if (signal?.aborted) throw new CursorAbortError();
785
+ closeCursorNativeTurnBlocks(turn);
786
+ const requests = collectCursorBridgeToolBatch(run);
787
+ emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
491
788
  return;
492
789
  }
493
790
  run.pendingEvents.shift();
494
- emitCursorNativeQueuedEvent(turn, event);
791
+ emitCursorLiveQueuedEvent(turn, event, run);
495
792
  }
496
793
 
497
794
  if (run.cancelled) {
498
795
  partial.stopReason = "aborted";
499
796
  stream.push({ type: "error", reason: "aborted", error: partial });
500
- await disposeCursorNativeRun(run);
797
+ await releaseCursorLiveRun(run);
501
798
  return;
502
799
  }
503
800
  if (run.errorMessage) {
504
801
  partial.stopReason = "error";
505
802
  partial.errorMessage = run.errorMessage;
506
803
  stream.push({ type: "error", reason: "error", error: partial });
507
- await disposeCursorNativeRun(run);
804
+ await releaseCursorLiveRun(run);
508
805
  return;
509
806
  }
510
807
  if (run.done) {
511
- let outputText = closeCursorNativeTurnBlocks(turn);
512
- if (!outputText) {
513
- outputText += await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(run.finalText ?? run.textDeltas.join("")));
808
+ closeCursorNativeTurnBlocks(turn);
809
+ const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
810
+ if (finalText) {
811
+ await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
514
812
  }
515
- setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
813
+ applyCursorApproximateUsage(partial, model, context, takeCursorLiveSessionInputTokens(run, toolResultInputTokens));
516
814
  partial.stopReason = "stop";
517
815
  stream.push({ type: "done", reason: "stop", message: partial });
518
- await disposeCursorNativeRun(run);
816
+ await releaseCursorLiveRun(run);
519
817
  return;
520
818
  }
521
819
 
@@ -523,21 +821,22 @@ async function emitCursorNativeRunNextTurn(
523
821
  }
524
822
  }
525
823
 
526
- async function replayPendingCursorNativeRun(
824
+ async function replayPendingCursorLiveRun(
527
825
  stream: AssistantMessageEventStream,
528
826
  partial: AssistantMessage,
827
+ model: Model<Api>,
529
828
  context: Context,
530
829
  signal?: AbortSignal,
531
830
  ): Promise<boolean> {
532
- const replayId = getPendingCursorNativeReplayId(context);
533
- if (!replayId) return false;
534
- const run = pendingCursorNativeRuns.get(replayId);
831
+ const run = getPendingCursorLiveRun(context);
535
832
  if (!run) return false;
536
833
  clearCursorNativeRunIdleDispose(run);
834
+ const consumed = consumeCursorLiveRunToolResults(run, context);
835
+ run.bridgeRun?.resolveToolResults(consumed.toolResults);
537
836
  try {
538
- await emitCursorNativeRunNextTurn(stream, partial, run, signal);
837
+ await emitCursorNativeRunNextTurn(stream, partial, model, context, run, consumed.toolResultInputTokens, signal);
539
838
  } catch (error) {
540
- if (error instanceof CursorAbortError) await disposeCursorNativeRun(run);
839
+ if (error instanceof CursorAbortError) await releaseCursorLiveRun(run);
541
840
  throw error;
542
841
  }
543
842
  return true;
@@ -553,10 +852,15 @@ export function streamCursor(
553
852
  (async () => {
554
853
  const partial = makeInitialMessage(model);
555
854
  let agent: SDKAgent | null = null;
556
- let activeNativeRun: CursorNativeLiveRun | undefined;
855
+ let activeLiveRun: CursorLiveRun | undefined;
856
+ let bridgeRun: CursorPiToolBridgeRun | undefined;
857
+ let liveRunForBridgeQueue: CursorLiveRun | undefined;
858
+ const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
557
859
  let resolvedApiKey: string | undefined;
860
+ let sessionAgentScopeKey = "";
558
861
  let abortSignal: AbortSignal | undefined;
559
862
  let abortListener: (() => void) | undefined;
863
+ let restoreCursorSdkOutputFilter: (() => void) | undefined;
560
864
 
561
865
  try {
562
866
  const throwIfAborted = (): void => {
@@ -566,7 +870,7 @@ export function streamCursor(
566
870
  stream.push({ type: "start", partial });
567
871
  throwIfAborted();
568
872
 
569
- if (await replayPendingCursorNativeRun(stream, partial, context, options?.signal)) {
873
+ if (await replayPendingCursorLiveRun(stream, partial, model, context, options?.signal)) {
570
874
  stream.end();
571
875
  return;
572
876
  }
@@ -575,26 +879,48 @@ export function streamCursor(
575
879
  if (!apiKey) throw new Error(MISSING_API_KEY_MESSAGE);
576
880
  resolvedApiKey = apiKey;
577
881
 
578
- // pi-ai Context/SimpleStreamOptions do not currently expose ExtensionContext.cwd;
579
- // provider calls use the process cwd until pi exposes a session cwd to streamSimple.
580
- const cwd = process.cwd();
882
+ // pi-ai Context/SimpleStreamOptions do not expose ExtensionContext.cwd; bridge via session_start
883
+ // until pi threads session cwd into streamSimple (cwd can change without a new session event).
884
+ const cwd = getCursorSessionCwd();
581
885
  const fastEnabled = getEffectiveFastForModelId(model.id);
582
886
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
583
887
  const settingSources = resolveCursorSettingSources();
584
888
 
585
- agent = await Agent.create({
889
+ installCursorMcpToolTimeoutOverride();
890
+ restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
891
+ const sessionAgentAcquireParams = {
586
892
  apiKey,
587
- model: selection,
588
- local: settingSources ? { cwd, settingSources } : { cwd },
589
- });
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;
590
910
  throwIfAborted();
591
911
 
592
- const prompt = buildCursorPrompt(context, {
593
- maxInputTokens: getPromptInputTokenBudget(model),
594
- charsPerToken: APPROX_CHARS_PER_TOKEN,
595
- imageTokenEstimate: IMAGE_TOKEN_ESTIMATE,
596
- });
597
- const promptInputTokens = estimatePromptInputTokens(prompt);
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);
598
924
  let thinkingContentIndex = -1;
599
925
  let activityTraceChars = 0;
600
926
  let activityTraceTruncated = false;
@@ -604,14 +930,18 @@ export function streamCursor(
604
930
  const nativeReplayId = createCursorNativeReplayId();
605
931
  const textDeltas: string[] = [];
606
932
  let nativeToolReplayStarted = false;
607
- const liveRun: CursorNativeLiveRun | undefined = useNativeToolReplay
933
+ const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
934
+ const liveRun: CursorLiveRun | undefined = useLiveRun
608
935
  ? {
609
- id: nativeReplayId,
936
+ id: useNativeToolReplay ? nativeReplayId : bridgeRun!.id,
610
937
  agent,
611
- promptInputTokens,
612
- promptInputTokensReported: false,
938
+ bridgeRun,
939
+ sessionBridgeRun,
940
+ sessionAgentScopeKey,
941
+ accounting: createCursorLiveRunAccountingState(promptInputTokens),
613
942
  pendingEvents: [],
614
943
  textDeltas,
944
+ emittedText: "",
615
945
  recordedToolDisplayIds: [],
616
946
  done: false,
617
947
  cancelled: false,
@@ -620,11 +950,20 @@ export function streamCursor(
620
950
  }
621
951
  : undefined;
622
952
  if (liveRun) {
623
- pendingCursorNativeRuns.set(liveRun.id, liveRun);
624
- activeNativeRun = liveRun;
953
+ pendingCursorLiveRuns.set(liveRun.id, liveRun);
954
+ activeLiveRun = liveRun;
955
+ liveRunForBridgeQueue = liveRun;
956
+ for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
957
+ queueCursorNativeEvent(liveRun, { type: "bridge-tool", request });
958
+ }
625
959
  }
626
960
  const startedToolCalls = new Map<string, unknown>();
961
+ const bridgeStartedToolCallIds = new Set<string>();
962
+ const activeShellCallIds = new Set<string>();
963
+ const ambiguousShellOutputCallIds = new Set<string>();
964
+ const shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
627
965
  const completedToolIdentities = new Set<string>();
966
+ let cursorPlanTextCandidate: string | undefined;
628
967
  const completedStartedToolFingerprints = new Set<string>();
629
968
  const completedFallbackToolFingerprints = new Set<string>();
630
969
 
@@ -684,6 +1023,16 @@ export function streamCursor(
684
1023
  closeTraceBlock();
685
1024
  };
686
1025
 
1026
+ const emitCursorToolTrace = (text: string): void => {
1027
+ const traceText = text.endsWith("\n") ? text : `${text}\n`;
1028
+ if (liveRun) {
1029
+ queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: traceText });
1030
+ queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
1031
+ return;
1032
+ }
1033
+ appendTraceBlock(traceText);
1034
+ };
1035
+
687
1036
  const closeTraceBlock = (): void => {
688
1037
  if (thinkingContentIndex < 0) return;
689
1038
  const block = partial.content[thinkingContentIndex];
@@ -720,10 +1069,80 @@ export function streamCursor(
720
1069
  }
721
1070
  };
722
1071
 
1072
+ const getStartedToolCallFingerprint = (toolCall: unknown): string => {
1073
+ return getToolFingerprint({ toolName: getCursorToolName(toolCall), args: getObjectField(toolCall, "args") });
1074
+ };
1075
+
1076
+ const clearStartedToolCall = (callId: string): void => {
1077
+ startedToolCalls.delete(callId);
1078
+ bridgeStartedToolCallIds.delete(callId);
1079
+ activeShellCallIds.delete(callId);
1080
+ ambiguousShellOutputCallIds.delete(callId);
1081
+ };
1082
+
1083
+ const takeBridgeStartedToolCallId = (callId: unknown): string | undefined => {
1084
+ if (typeof callId !== "string" || !bridgeStartedToolCallIds.has(callId)) return undefined;
1085
+ bridgeStartedToolCallIds.delete(callId);
1086
+ return callId;
1087
+ };
1088
+
1089
+ const takeShellOutputDeltas = (callId: string): CursorShellOutputDeltas | undefined => {
1090
+ const deltas = shellOutputDeltasByCallId.get(callId);
1091
+ shellOutputDeltasByCallId.delete(callId);
1092
+ return deltas;
1093
+ };
1094
+
1095
+ const appendShellOutputDelta = (delta: CursorShellOutputDelta): void => {
1096
+ if (activeShellCallIds.size !== 1) {
1097
+ for (const activeCallId of activeShellCallIds) {
1098
+ ambiguousShellOutputCallIds.add(activeCallId);
1099
+ shellOutputDeltasByCallId.delete(activeCallId);
1100
+ }
1101
+ return;
1102
+ }
1103
+ const [callId] = activeShellCallIds;
1104
+ if (!callId || ambiguousShellOutputCallIds.has(callId)) return;
1105
+ let deltas = shellOutputDeltasByCallId.get(callId);
1106
+ if (!deltas) {
1107
+ deltas = { stdout: [], stderr: [] };
1108
+ shellOutputDeltasByCallId.set(callId, deltas);
1109
+ }
1110
+ deltas[delta.stream].push(delta.data);
1111
+ };
1112
+
1113
+ const removeStartedToolCallForStep = (toolCall: unknown, stepId: unknown): string | undefined => {
1114
+ if (typeof stepId === "string" && startedToolCalls.has(stepId)) {
1115
+ clearStartedToolCall(stepId);
1116
+ return stepId;
1117
+ }
1118
+ const fingerprint = getStartedToolCallFingerprint(toolCall);
1119
+ for (const [callId, startedToolCall] of startedToolCalls) {
1120
+ if (getStartedToolCallFingerprint(startedToolCall) !== fingerprint) continue;
1121
+ clearStartedToolCall(callId);
1122
+ return callId;
1123
+ }
1124
+ return undefined;
1125
+ };
1126
+
1127
+ const discardIncompleteStartedToolCalls = (): void => {
1128
+ startedToolCalls.clear();
1129
+ bridgeStartedToolCallIds.clear();
1130
+ activeShellCallIds.clear();
1131
+ ambiguousShellOutputCallIds.clear();
1132
+ shellOutputDeltasByCallId.clear();
1133
+ };
1134
+
723
1135
  const handleCompletedToolCall = (
724
1136
  toolCall: unknown,
725
1137
  options: { identity?: string; source?: "started" | "fallback" } = {},
726
1138
  ): void => {
1139
+ const planText = getCursorCreatePlanText(toolCall);
1140
+ if (planText) cursorPlanTextCandidate = scrubSensitiveText(planText, resolvedApiKey);
1141
+
1142
+ if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
1143
+ if (options.identity) completedToolIdentities.add(options.identity);
1144
+ return;
1145
+ }
727
1146
  const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
728
1147
  const display = buildCursorPiToolDisplay(toolCall, { cwd });
729
1148
  const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
@@ -740,11 +1159,10 @@ export function streamCursor(
740
1159
  completedFallbackToolFingerprints.add(fingerprint);
741
1160
  }
742
1161
 
743
- if (useNativeToolReplay && canRenderCursorToolNatively(display.toolName) && liveRun) {
744
- if (!nativeToolReplayStarted && textDeltas.length > 0) {
745
- for (const text of textDeltas) queueCursorNativeEvent(liveRun, { type: "text-delta", text });
746
- textDeltas.length = 0;
747
- }
1162
+ const nativeRenderable = canRenderCursorToolNatively(display.toolName);
1163
+ const route = useNativeToolReplay && nativeRenderable && liveRun ? "native_replay" : "trace";
1164
+
1165
+ if (route === "native_replay" && liveRun) {
748
1166
  nativeToolReplayStarted = true;
749
1167
  const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
750
1168
  queueCursorNativeEvent(liveRun, {
@@ -759,7 +1177,7 @@ export function streamCursor(
759
1177
  return;
760
1178
  }
761
1179
 
762
- appendTraceBlock(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
1180
+ emitCursorToolTrace(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
763
1181
  };
764
1182
 
765
1183
  const onDelta = (args: { update: InteractionUpdate }): void => {
@@ -767,36 +1185,50 @@ export function streamCursor(
767
1185
 
768
1186
  if (update.type === "text-delta") {
769
1187
  textDeltas.push(update.text);
770
- if (liveRun && nativeToolReplayStarted) {
1188
+ if (liveRun) {
771
1189
  queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
772
- } else if (!useNativeToolReplay) {
1190
+ } else {
773
1191
  appendLiveTextDelta(update.text);
774
1192
  }
775
1193
  } else if (update.type === "thinking-delta") {
776
- if (liveRun && nativeToolReplayStarted) {
1194
+ if (liveRun) {
777
1195
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
778
1196
  } else {
779
1197
  appendTraceDelta(update.text);
780
1198
  }
781
1199
  } else if (update.type === "thinking-completed") {
782
- if (liveRun && nativeToolReplayStarted) {
1200
+ if (liveRun) {
783
1201
  queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
784
1202
  } else {
785
1203
  closeTraceBlock();
786
1204
  }
787
1205
  } else if (update.type === "tool-call-started") {
788
- startedToolCalls.set(update.callId, update.toolCall);
1206
+ if (liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
1207
+ if (typeof update.callId === "string") bridgeStartedToolCallIds.add(update.callId);
1208
+ } else {
1209
+ startedToolCalls.set(update.callId, update.toolCall);
1210
+ if (isCursorShellToolCall(update.toolCall)) activeShellCallIds.add(update.callId);
1211
+ }
789
1212
  } else if (update.type === "tool-call-completed") {
790
- const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
791
- startedToolCalls.delete(update.callId);
792
1213
  const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
793
- handleCompletedToolCall(mergedToolCall, {
1214
+ const bridgeStartedCallId = takeBridgeStartedToolCallId(update.callId);
1215
+ if (bridgeStartedCallId) {
1216
+ completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
1217
+ return;
1218
+ }
1219
+ const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
1220
+ clearStartedToolCall(update.callId);
1221
+ const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(mergedToolCall, takeShellOutputDeltas(update.callId));
1222
+ handleCompletedToolCall(toolCallWithShellOutput, {
794
1223
  identity,
795
1224
  source: identity ? "started" : "fallback",
796
1225
  });
1226
+ } else if (update.type === "shell-output-delta") {
1227
+ const delta = getCursorShellOutputDelta(update);
1228
+ if (delta) appendShellOutputDelta(delta);
797
1229
  } else if (update.type === "summary") {
798
1230
  const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
799
- if (liveRun && nativeToolReplayStarted) {
1231
+ if (liveRun) {
800
1232
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
801
1233
  } else {
802
1234
  appendTraceDelta(summary);
@@ -805,24 +1237,43 @@ export function streamCursor(
805
1237
  // Cursor turn-ended usage is intentionally not copied into pi usage: the SDK reports
806
1238
  // cumulative internal agent/tool/cache tokens, not the replayable pi prompt context.
807
1239
  // partial-tool-call, summary-started, summary-completed, turn-ended,
808
- // shell-output-delta, token-delta, step-* are intentionally not surfaced.
1240
+ // token-delta, step-* are intentionally not surfaced.
809
1241
  };
810
1242
 
811
1243
  const onStep = (args: { step: unknown }): void => {
1244
+ const stepType = getObjectField(args.step, "type");
812
1245
  const step = getObjectField(args.step, "message") ? args.step : undefined;
813
- if (getObjectField(args.step, "type") !== "toolCall") return;
814
- const toolCall = getObjectField(step, "message");
1246
+ const rawStepToolCall = getObjectField(step, "message");
1247
+ if (stepType !== "toolCall") return;
1248
+ const toolCall = rawStepToolCall;
815
1249
  const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
816
1250
  if (toolCall) {
817
- handleCompletedToolCall(toolCall, {
818
- identity: typeof stepId === "string" ? `cursor-tool:${stepId}` : undefined,
1251
+ const bridgeStartedCallId = takeBridgeStartedToolCallId(stepId);
1252
+ if (bridgeStartedCallId) {
1253
+ completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
1254
+ return;
1255
+ }
1256
+ const matchedStartedCallId = removeStartedToolCallForStep(toolCall, stepId);
1257
+ const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(
1258
+ toolCall,
1259
+ matchedStartedCallId ? takeShellOutputDeltas(matchedStartedCallId) : undefined,
1260
+ );
1261
+ if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
1262
+ if (matchedStartedCallId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
1263
+ return;
1264
+ }
1265
+ const identityId = typeof stepId === "string" ? stepId : matchedStartedCallId;
1266
+ handleCompletedToolCall(toolCallWithShellOutput, {
1267
+ identity: identityId ? `cursor-tool:${identityId}` : undefined,
819
1268
  });
1269
+ if (matchedStartedCallId && matchedStartedCallId !== stepId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
820
1270
  }
821
1271
  };
822
1272
 
823
1273
  // Handle abort signal
824
1274
  let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
825
1275
  abortListener = () => {
1276
+ activeLiveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
826
1277
  if (run) {
827
1278
  run.cancel().catch(() => {});
828
1279
  }
@@ -835,26 +1286,38 @@ export function streamCursor(
835
1286
  { text: prompt.text, images: prompt.images.length > 0 ? prompt.images : undefined },
836
1287
  { onDelta, onStep },
837
1288
  );
1289
+ if (liveRun) liveRun.sdkRun = run;
838
1290
  if (options?.signal?.aborted) {
839
1291
  await run.cancel().catch(() => {});
840
1292
  throw new CursorAbortError();
841
1293
  }
842
1294
 
843
- if (useNativeToolReplay && liveRun) {
1295
+ if (liveRun) {
844
1296
  void run
845
1297
  .wait()
846
1298
  .then(async (result) => {
847
1299
  if (liveRun.disposed) return;
1300
+ discardIncompleteStartedToolCalls();
848
1301
  await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
849
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
+ }
850
1308
  liveRun.cancelled = result.status === "cancelled";
851
- liveRun.finalText = hasUsableText(result.result) ? result.result : liveRun.textDeltas.join("");
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
+ }
852
1314
  liveRun.done = true;
853
1315
  notifyCursorNativeRun(liveRun);
854
1316
  scheduleCursorNativeRunIdleDispose(liveRun);
855
1317
  })
856
- .catch((error: unknown) => {
1318
+ .catch(async (error: unknown) => {
857
1319
  if (liveRun.disposed) return;
1320
+ await abandonSessionCursorAgent(sessionAgentScopeKey);
858
1321
  liveRun.errorMessage = sanitizeError(error, resolvedApiKey ?? options?.apiKey);
859
1322
  notifyCursorNativeRun(liveRun);
860
1323
  scheduleCursorNativeRunIdleDispose(liveRun);
@@ -862,11 +1325,11 @@ export function streamCursor(
862
1325
 
863
1326
  try {
864
1327
  await waitForCursorNativeRunProgress(liveRun, options?.signal);
865
- await settleCursorNativeToolBatch(liveRun);
1328
+ await settleCursorLiveToolBatch(liveRun);
866
1329
  closeTraceBlock();
867
- await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
1330
+ await emitCursorNativeRunNextTurn(stream, partial, model, context, liveRun, 0, options?.signal);
868
1331
  } catch (error) {
869
- if (error instanceof CursorAbortError) await disposeCursorNativeRun(liveRun);
1332
+ if (error instanceof CursorAbortError) await releaseCursorLiveRun(liveRun);
870
1333
  throw error;
871
1334
  }
872
1335
  agent = null;
@@ -874,6 +1337,7 @@ export function streamCursor(
874
1337
  }
875
1338
 
876
1339
  const result = await run.wait();
1340
+ discardIncompleteStartedToolCalls();
877
1341
  await cacheSdkContextWindow(agent.agentId, model.id);
878
1342
 
879
1343
  // Close any open thinking/activity trace, then use the final run result only when
@@ -881,14 +1345,26 @@ export function streamCursor(
881
1345
  closeTraceBlock();
882
1346
 
883
1347
  if (result.status === "cancelled") {
1348
+ await abandonSessionCursorAgent(sessionAgentScopeKey);
884
1349
  partial.stopReason = "aborted";
885
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 });
886
1356
  } else {
887
- const finalText = flushText(textDeltas.length === 0 && hasUsableText(result.result) ? [result.result] : []);
888
- setApproximateUsage(partial, promptInputTokens, finalText);
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);
889
1363
  stream.push({ type: "done", reason: "stop", message: partial });
890
1364
  }
891
1365
  } catch (error) {
1366
+ if (activeLiveRun && !activeLiveRun.disposed) await releaseCursorLiveRun(activeLiveRun);
1367
+ else await abandonSessionCursorAgent(sessionAgentScopeKey);
892
1368
  if (error instanceof CursorAbortError) {
893
1369
  partial.stopReason = "aborted";
894
1370
  stream.push({ type: "error", reason: "aborted", error: partial });
@@ -898,20 +1374,11 @@ export function streamCursor(
898
1374
  stream.push({ type: "error", reason: "error", error: partial });
899
1375
  }
900
1376
  } finally {
901
- if (activeNativeRun?.disposed) agent = null;
1377
+ restoreCursorSdkOutputFilter?.();
902
1378
 
903
1379
  if (abortSignal && abortListener) {
904
1380
  abortSignal.removeEventListener("abort", abortListener);
905
1381
  }
906
-
907
- if (agent) {
908
- try {
909
- await agent[Symbol.asyncDispose]();
910
- } catch {
911
- // disposal failure should not mask original error
912
- }
913
- agent = null;
914
- }
915
1382
  }
916
1383
 
917
1384
  stream.end();
@@ -922,11 +1389,12 @@ export function streamCursor(
922
1389
 
923
1390
  export const __testUtils = {
924
1391
  DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
925
- pendingCursorNativeRunCount: () => pendingCursorNativeRuns.size,
1392
+ pendingCursorNativeRunCount: () => pendingCursorLiveRuns.size,
926
1393
  setCursorNativeReplayIdleDisposeMs: (value: number) => {
927
1394
  cursorNativeReplayIdleDisposeMs = value;
928
1395
  },
929
1396
  resetCursorNativeReplayIdleDisposeMs: () => {
930
1397
  cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
931
1398
  },
1399
+ resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
932
1400
  };