pi-cursor-sdk 0.1.13 → 0.1.15

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.
@@ -7,13 +7,21 @@ import {
7
7
  type SimpleStreamOptions,
8
8
  type AssistantMessage,
9
9
  } from "@earendil-works/pi-ai";
10
+ import { AsyncLocalStorage } from "node:async_hooks";
10
11
  import { Agent, createAgentPlatform } from "@cursor/sdk";
11
12
  import type { InteractionUpdate, SDKAgent, SettingSource } from "@cursor/sdk";
13
+ import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
12
14
  import { buildCursorPrompt, type CursorPrompt } from "./context.js";
15
+ import {
16
+ getRegisteredCursorPiToolBridge,
17
+ type CursorPiBridgeToolRequest,
18
+ type CursorPiToolBridgeRun,
19
+ } from "./cursor-pi-tool-bridge.js";
20
+ import { getCursorSessionCwd } from "./cursor-session-cwd.js";
13
21
  import { getEffectiveFastForModelId } from "./cursor-state.js";
14
22
  import { buildCursorModelSelection } from "./model-discovery.js";
15
23
  import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-window-cache.js";
16
- import { buildCursorPiToolDisplay, formatCursorToolTranscript, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
24
+ import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
17
25
  import {
18
26
  canRenderCursorToolNatively,
19
27
  isCursorNativeToolDisplayRuntimeEnabled,
@@ -62,20 +70,24 @@ const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
62
70
  const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
63
71
  const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
64
72
  const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
73
+ const cursorSdkOutputSuppression = new AsyncLocalStorage<boolean>();
65
74
 
66
- type CursorNativeQueuedEvent =
75
+ type CursorLiveQueuedEvent =
67
76
  | { type: "thinking-delta"; text: string }
68
77
  | { type: "thinking-completed" }
69
78
  | { type: "text-delta"; text: string }
70
- | { type: "tool"; tool: CursorNativeToolDisplayItem };
79
+ | { type: "tool"; tool: CursorNativeToolDisplayItem }
80
+ | { type: "bridge-tool"; request: CursorPiBridgeToolRequest };
71
81
 
72
- interface CursorNativeLiveRun {
82
+ interface CursorLiveRun {
73
83
  id: string;
74
84
  agent: SDKAgent;
85
+ bridgeRun?: CursorPiToolBridgeRun;
75
86
  promptInputTokens: number;
76
87
  promptInputTokensReported: boolean;
77
- pendingEvents: CursorNativeQueuedEvent[];
88
+ pendingEvents: CursorLiveQueuedEvent[];
78
89
  textDeltas: string[];
90
+ emittedText: string;
79
91
  recordedToolDisplayIds: string[];
80
92
  finalText?: string;
81
93
  done: boolean;
@@ -86,7 +98,7 @@ interface CursorNativeLiveRun {
86
98
  waiters: Set<() => void>;
87
99
  }
88
100
 
89
- interface CursorNativeTurnState {
101
+ interface CursorLiveTurnState {
90
102
  stream: AssistantMessageEventStream;
91
103
  partial: AssistantMessage;
92
104
  thinkingContentIndex: number;
@@ -95,7 +107,7 @@ interface CursorNativeTurnState {
95
107
 
96
108
  let cursorNativeReplayCounter = 0;
97
109
  let cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
98
- const pendingCursorNativeRuns = new Map<string, CursorNativeLiveRun>();
110
+ const pendingCursorLiveRuns = new Map<string, CursorLiveRun>();
99
111
 
100
112
  function escapeRegExp(value: string): string {
101
113
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -134,7 +146,7 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
134
146
 
135
147
  function resolveCursorSettingSources(): SettingSource[] | undefined {
136
148
  const raw = process.env[CURSOR_SETTING_SOURCES_ENV]?.trim();
137
- if (!raw) return undefined;
149
+ if (!raw) return ["all"];
138
150
  const normalized = raw.toLowerCase();
139
151
  if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
140
152
  if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
@@ -153,6 +165,78 @@ function sanitizeError(error: unknown, apiKey?: string): string {
153
165
  return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
154
166
  }
155
167
 
168
+ function isCursorSdkOutputSuppressed(): boolean {
169
+ return cursorSdkOutputSuppression.getStore() === true;
170
+ }
171
+
172
+ function suppressCursorSdkOutput<T>(operation: () => Promise<T>): Promise<T> {
173
+ return cursorSdkOutputSuppression.run(true, operation);
174
+ }
175
+
176
+ const CURSOR_SDK_STARTUP_NOISE_PATTERNS = [
177
+ "managed_skills.",
178
+ "CursorPluginsAgentSkillsService load completed",
179
+ "LocalCursorRulesService load completed",
180
+ "AgentSkillsCursorRulesService load completed",
181
+ ];
182
+
183
+ function isCursorSdkStartupNoise(text: string): boolean {
184
+ return CURSOR_SDK_STARTUP_NOISE_PATTERNS.some((pattern) => text.includes(pattern));
185
+ }
186
+
187
+ function createFilteredProcessWrite<TWrite extends typeof process.stdout.write>(write: TWrite, stream: NodeJS.WriteStream): TWrite {
188
+ return ((
189
+ chunk: string | Uint8Array,
190
+ encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
191
+ callback?: (error?: Error | null) => void,
192
+ ): boolean => {
193
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
194
+ if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) {
195
+ const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
196
+ done?.();
197
+ return true;
198
+ }
199
+ return write.call(stream, chunk as string, encodingOrCallback as BufferEncoding, callback);
200
+ }) as TWrite;
201
+ }
202
+
203
+ function createFilteredConsoleMethod<TMethod extends typeof console.log>(method: TMethod): TMethod {
204
+ return ((...args: Parameters<TMethod>): void => {
205
+ const text = args.map((arg) => (typeof arg === "string" ? arg : String(arg))).join(" ");
206
+ if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) return;
207
+ method(...args);
208
+ }) as TMethod;
209
+ }
210
+
211
+ function installCursorSdkOutputFilter(): () => void {
212
+ const stdoutWrite = process.stdout.write;
213
+ const stderrWrite = process.stderr.write;
214
+ const consoleLog = console.log;
215
+ const consoleInfo = console.info;
216
+ const consoleWarn = console.warn;
217
+ const consoleError = console.error;
218
+ const consoleDebug = console.debug;
219
+ process.stdout.write = createFilteredProcessWrite(stdoutWrite, process.stdout);
220
+ process.stderr.write = createFilteredProcessWrite(stderrWrite, process.stderr) as typeof process.stderr.write;
221
+ console.log = createFilteredConsoleMethod(consoleLog);
222
+ console.info = createFilteredConsoleMethod(consoleInfo);
223
+ console.warn = createFilteredConsoleMethod(consoleWarn);
224
+ console.error = createFilteredConsoleMethod(consoleError);
225
+ console.debug = createFilteredConsoleMethod(consoleDebug);
226
+ let restored = false;
227
+ return () => {
228
+ if (restored) return;
229
+ restored = true;
230
+ process.stdout.write = stdoutWrite;
231
+ process.stderr.write = stderrWrite;
232
+ console.log = consoleLog;
233
+ console.info = consoleInfo;
234
+ console.warn = consoleWarn;
235
+ console.error = consoleError;
236
+ console.debug = consoleDebug;
237
+ };
238
+ }
239
+
156
240
  function getObjectField(value: unknown, field: string): unknown {
157
241
  if (!value || typeof value !== "object") return undefined;
158
242
  return (value as Record<string, unknown>)[field];
@@ -163,6 +247,7 @@ function getCursorToolName(toolCall: unknown): string {
163
247
  const data = toolCall as Record<string, unknown>;
164
248
  if (typeof data.name === "string") return data.name;
165
249
  if (typeof data.type === "string") return data.type;
250
+ if (typeof data.toolName === "string") return data.toolName;
166
251
  return "unknown";
167
252
  }
168
253
 
@@ -215,6 +300,63 @@ function hasUsableText(value: string | undefined): value is string {
215
300
  return typeof value === "string" && value.trim().length > 0;
216
301
  }
217
302
 
303
+ interface CursorShellOutputDelta {
304
+ stream: "stdout" | "stderr";
305
+ data: string;
306
+ }
307
+
308
+ interface CursorShellOutputDeltas {
309
+ stdout: string[];
310
+ stderr: string[];
311
+ }
312
+
313
+ function isCursorShellToolCall(toolCall: unknown): boolean {
314
+ const normalizedName = getCursorToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase();
315
+ return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
316
+ }
317
+
318
+ function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
319
+ if (update.type !== "shell-output-delta") return undefined;
320
+ const event = getObjectField(update, "event");
321
+ const eventCase = getObjectField(event, "case");
322
+ if (eventCase !== "stdout" && eventCase !== "stderr") return undefined;
323
+ const value = getObjectField(event, "value");
324
+ const data = getObjectField(value, "data");
325
+ if (typeof data !== "string" || data.length === 0) return undefined;
326
+ return { stream: eventCase, data };
327
+ }
328
+
329
+ function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: CursorShellOutputDeltas | undefined): unknown {
330
+ if (!deltas) return toolCall;
331
+ const stdout = deltas.stdout.join("");
332
+ const stderr = deltas.stderr.join("");
333
+ if (!hasUsableText(stdout) && !hasUsableText(stderr)) return toolCall;
334
+
335
+ const toolRecord = toolCall && typeof toolCall === "object" && !Array.isArray(toolCall) ? (toolCall as Record<string, unknown>) : undefined;
336
+ const result = getObjectField(toolRecord, "result");
337
+ const resultRecord = result && typeof result === "object" && !Array.isArray(result) ? (result as Record<string, unknown>) : undefined;
338
+ if (!toolRecord || !resultRecord || resultRecord.status !== "success") return toolCall;
339
+
340
+ const value = getObjectField(resultRecord, "value");
341
+ const valueRecord = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
342
+ const completedStdout = getObjectField(valueRecord, "stdout");
343
+ const completedStderr = getObjectField(valueRecord, "stderr");
344
+ if (hasUsableText(typeof completedStdout === "string" ? completedStdout : undefined)) return toolCall;
345
+ if (hasUsableText(typeof completedStderr === "string" ? completedStderr : undefined)) return toolCall;
346
+
347
+ return {
348
+ ...toolRecord,
349
+ result: {
350
+ ...resultRecord,
351
+ value: {
352
+ ...(valueRecord ?? {}),
353
+ stdout,
354
+ stderr,
355
+ },
356
+ },
357
+ };
358
+ }
359
+
218
360
  function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
219
361
  if (typeof value === "string") return scrubSensitiveText(value, apiKey);
220
362
  if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
@@ -233,12 +375,18 @@ function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | und
233
375
  return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
234
376
  }
235
377
 
236
- function getPendingCursorNativeReplayId(context: Context): string | undefined {
378
+ function getPendingCursorLiveRun(context: Context): CursorLiveRun | undefined {
237
379
  for (let index = context.messages.length - 1; index >= 0; index -= 1) {
238
380
  const message = context.messages[index];
239
381
  if (message.role !== "toolResult") break;
240
382
  const replayId = getCursorNativeReplayIdFromToolCallId(message.toolCallId);
241
- if (replayId && pendingCursorNativeRuns.has(replayId)) return replayId;
383
+ if (replayId) {
384
+ const replayRun = pendingCursorLiveRuns.get(replayId);
385
+ if (replayRun) return replayRun;
386
+ }
387
+ for (const run of pendingCursorLiveRuns.values()) {
388
+ if (run.bridgeRun?.hasPendingPiToolCallId(message.toolCallId)) return run;
389
+ }
242
390
  }
243
391
  return undefined;
244
392
  }
@@ -278,23 +426,23 @@ async function emitTextDeltas(
278
426
  return block.text;
279
427
  }
280
428
 
281
- function notifyCursorNativeRun(run: CursorNativeLiveRun): void {
429
+ function notifyCursorNativeRun(run: CursorLiveRun): void {
282
430
  for (const waiter of run.waiters) waiter();
283
431
  run.waiters.clear();
284
432
  }
285
433
 
286
- function queueCursorNativeEvent(run: CursorNativeLiveRun, event: CursorNativeQueuedEvent): void {
434
+ function queueCursorNativeEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void {
287
435
  run.pendingEvents.push(event);
288
436
  notifyCursorNativeRun(run);
289
437
  }
290
438
 
291
- function clearCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
439
+ function clearCursorNativeRunIdleDispose(run: CursorLiveRun): void {
292
440
  if (!run.idleDisposeTimer) return;
293
441
  clearTimeout(run.idleDisposeTimer);
294
442
  run.idleDisposeTimer = undefined;
295
443
  }
296
444
 
297
- function scheduleCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
445
+ function scheduleCursorNativeRunIdleDispose(run: CursorLiveRun): void {
298
446
  if (run.disposed) return;
299
447
  clearCursorNativeRunIdleDispose(run);
300
448
  run.idleDisposeTimer = setTimeout(() => {
@@ -303,11 +451,12 @@ function scheduleCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
303
451
  run.idleDisposeTimer.unref?.();
304
452
  }
305
453
 
306
- function isCursorNativeRunReady(run: CursorNativeLiveRun): boolean {
454
+ function isCursorNativeRunReady(run: CursorLiveRun): boolean {
307
455
  return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
308
456
  }
309
457
 
310
- async function waitForCursorNativeRunProgress(run: CursorNativeLiveRun, signal?: AbortSignal): Promise<void> {
458
+ async function waitForCursorNativeRunProgress(run: CursorLiveRun, signal?: AbortSignal): Promise<void> {
459
+ if (signal?.aborted) throw new CursorAbortError();
311
460
  if (isCursorNativeRunReady(run)) return;
312
461
  await new Promise<void>((resolve, reject) => {
313
462
  let waiter: (() => void) | undefined;
@@ -324,16 +473,21 @@ async function waitForCursorNativeRunProgress(run: CursorNativeLiveRun, signal?:
324
473
  resolve();
325
474
  };
326
475
  run.waiters.add(waiter);
476
+ if (signal?.aborted) {
477
+ onAbort();
478
+ return;
479
+ }
327
480
  signal?.addEventListener("abort", onAbort, { once: true });
328
481
  });
329
482
  }
330
483
 
331
- async function settleCursorNativeToolBatch(run: CursorNativeLiveRun): Promise<void> {
332
- if (run.pendingEvents[0]?.type !== "tool") return;
484
+ async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<void> {
485
+ const eventType = run.pendingEvents[0]?.type;
486
+ if (eventType !== "tool" && eventType !== "bridge-tool") return;
333
487
  await new Promise((resolve) => setTimeout(resolve, 75));
334
488
  }
335
489
 
336
- function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
490
+ function closeCursorNativeThinkingBlock(turn: CursorLiveTurnState): void {
337
491
  if (turn.thinkingContentIndex < 0) return;
338
492
  const block = turn.partial.content[turn.thinkingContentIndex];
339
493
  if (block.type === "thinking") {
@@ -347,7 +501,7 @@ function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
347
501
  turn.thinkingContentIndex = -1;
348
502
  }
349
503
 
350
- function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
504
+ function closeCursorNativeTextBlock(turn: CursorLiveTurnState): string {
351
505
  if (turn.textContentIndex < 0) return "";
352
506
  const contentIndex = turn.textContentIndex;
353
507
  const block = turn.partial.content[contentIndex];
@@ -362,12 +516,12 @@ function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
362
516
  return block.text;
363
517
  }
364
518
 
365
- function closeCursorNativeTurnBlocks(turn: CursorNativeTurnState): string {
519
+ function closeCursorNativeTurnBlocks(turn: CursorLiveTurnState): string {
366
520
  closeCursorNativeThinkingBlock(turn);
367
521
  return closeCursorNativeTextBlock(turn);
368
522
  }
369
523
 
370
- function emitCursorNativeThinkingDelta(turn: CursorNativeTurnState, delta: string): void {
524
+ function emitCursorNativeThinkingDelta(turn: CursorLiveTurnState, delta: string): void {
371
525
  closeCursorNativeTextBlock(turn);
372
526
  if (turn.thinkingContentIndex < 0) {
373
527
  turn.thinkingContentIndex = turn.partial.content.length;
@@ -380,7 +534,7 @@ function emitCursorNativeThinkingDelta(turn: CursorNativeTurnState, delta: strin
380
534
  turn.stream.push({ type: "thinking_delta", contentIndex: turn.thinkingContentIndex, delta, partial: turn.partial });
381
535
  }
382
536
 
383
- function emitCursorNativeTextDelta(turn: CursorNativeTurnState, delta: string): void {
537
+ function emitCursorNativeTextDelta(turn: CursorLiveTurnState, delta: string): void {
384
538
  closeCursorNativeThinkingBlock(turn);
385
539
  if (turn.textContentIndex < 0) {
386
540
  turn.textContentIndex = turn.partial.content.length;
@@ -393,20 +547,22 @@ function emitCursorNativeTextDelta(turn: CursorNativeTurnState, delta: string):
393
547
  turn.stream.push({ type: "text_delta", contentIndex: turn.textContentIndex, delta, partial: turn.partial });
394
548
  }
395
549
 
396
- function emitCursorNativeQueuedEvent(
397
- turn: CursorNativeTurnState,
398
- event: Exclude<CursorNativeQueuedEvent, { type: "tool" }>,
550
+ function emitCursorLiveQueuedEvent(
551
+ turn: CursorLiveTurnState,
552
+ event: Exclude<CursorLiveQueuedEvent, { type: "tool" } | { type: "bridge-tool" }>,
553
+ run?: CursorLiveRun,
399
554
  ): void {
400
555
  if (event.type === "thinking-delta") {
401
556
  emitCursorNativeThinkingDelta(turn, event.text);
402
557
  } else if (event.type === "thinking-completed") {
403
558
  closeCursorNativeThinkingBlock(turn);
404
559
  } else if (event.type === "text-delta") {
560
+ if (run) run.emittedText += event.text;
405
561
  emitCursorNativeTextDelta(turn, event.text);
406
562
  }
407
563
  }
408
564
 
409
- function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToolDisplayItem[] {
565
+ function collectCursorNativeToolBatch(run: CursorLiveRun): CursorNativeToolDisplayItem[] {
410
566
  const tools: CursorNativeToolDisplayItem[] = [];
411
567
  while (run.pendingEvents[0]?.type === "tool") {
412
568
  const event = run.pendingEvents.shift();
@@ -415,7 +571,41 @@ function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToo
415
571
  return tools;
416
572
  }
417
573
 
418
- function takeCursorNativePromptInputTokens(run: CursorNativeLiveRun): number {
574
+ function collectCursorBridgeToolBatch(run: CursorLiveRun): CursorPiBridgeToolRequest[] {
575
+ const requests: CursorPiBridgeToolRequest[] = [];
576
+ while (run.pendingEvents[0]?.type === "bridge-tool") {
577
+ const event = run.pendingEvents.shift();
578
+ if (event?.type === "bridge-tool") requests.push(event.request);
579
+ }
580
+ return requests;
581
+ }
582
+
583
+ function trimAlreadyEmittedCursorText(text: string, emittedText: string): string {
584
+ if (!text || !emittedText) return text;
585
+ if (text === emittedText) return "";
586
+ if (text.startsWith(emittedText)) return text.slice(emittedText.length);
587
+ if (emittedText.endsWith(text)) return "";
588
+ if (text.trim() === emittedText.trim()) return "";
589
+ if (emittedText.trim().endsWith(text.trim())) return "";
590
+ return text;
591
+ }
592
+
593
+ function selectCursorFinalText(
594
+ resultText: unknown,
595
+ textDeltas: readonly string[],
596
+ emittedText: string,
597
+ fallbackText?: string,
598
+ ): string {
599
+ const candidates = [typeof resultText === "string" ? resultText : undefined, fallbackText, textDeltas.join("")];
600
+ for (const candidate of candidates) {
601
+ if (!hasUsableText(candidate)) continue;
602
+ const trimmedCandidate = trimAlreadyEmittedCursorText(candidate, emittedText);
603
+ if (hasUsableText(trimmedCandidate)) return trimmedCandidate;
604
+ }
605
+ return "";
606
+ }
607
+
608
+ function takeCursorNativePromptInputTokens(run: CursorLiveRun): number {
419
609
  // Native replay can split one Cursor run into multiple pi turns; count prompt input once.
420
610
  if (run.promptInputTokensReported) return 0;
421
611
  run.promptInputTokensReported = true;
@@ -425,7 +615,7 @@ function takeCursorNativePromptInputTokens(run: CursorNativeLiveRun): number {
425
615
  function emitCursorNativeToolUseTurn(
426
616
  stream: AssistantMessageEventStream,
427
617
  partial: AssistantMessage,
428
- run: CursorNativeLiveRun,
618
+ run: CursorLiveRun,
429
619
  tools: CursorNativeToolDisplayItem[],
430
620
  outputText: string,
431
621
  ): void {
@@ -452,14 +642,46 @@ function emitCursorNativeToolUseTurn(
452
642
  scheduleCursorNativeRunIdleDispose(run);
453
643
  }
454
644
 
455
- async function disposeCursorNativeRun(run: CursorNativeLiveRun): Promise<void> {
645
+ function emitCursorBridgeToolUseTurn(
646
+ stream: AssistantMessageEventStream,
647
+ partial: AssistantMessage,
648
+ run: CursorLiveRun,
649
+ requests: CursorPiBridgeToolRequest[],
650
+ outputText: string,
651
+ ): void {
652
+ for (const request of requests) {
653
+ const contentIndex = partial.content.length;
654
+ partial.content.push({
655
+ type: "toolCall",
656
+ id: request.piToolCallId,
657
+ name: request.piToolName,
658
+ arguments: request.args,
659
+ });
660
+ stream.push({ type: "toolcall_start", contentIndex, partial });
661
+ stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(request.args), partial });
662
+ const block = partial.content[contentIndex];
663
+ if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
664
+ }
665
+ setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
666
+ partial.stopReason = "toolUse";
667
+ stream.push({ type: "done", reason: "toolUse", message: partial });
668
+ scheduleCursorNativeRunIdleDispose(run);
669
+ }
670
+
671
+ async function disposeCursorNativeRun(run: CursorLiveRun): Promise<void> {
456
672
  if (run.disposed) return;
457
673
  run.disposed = true;
458
- pendingCursorNativeRuns.delete(run.id);
674
+ pendingCursorLiveRuns.delete(run.id);
459
675
  clearCursorNativeRunIdleDispose(run);
676
+ run.bridgeRun?.cancel("Cursor live run disposed");
460
677
  for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
461
678
  run.recordedToolDisplayIds = [];
462
679
  run.waiters.clear();
680
+ try {
681
+ await run.bridgeRun?.dispose();
682
+ } catch {
683
+ // bridge disposal failure should not mask the provider result
684
+ }
463
685
  try {
464
686
  await run.agent[Symbol.asyncDispose]();
465
687
  } catch {
@@ -470,10 +692,10 @@ async function disposeCursorNativeRun(run: CursorNativeLiveRun): Promise<void> {
470
692
  async function emitCursorNativeRunNextTurn(
471
693
  stream: AssistantMessageEventStream,
472
694
  partial: AssistantMessage,
473
- run: CursorNativeLiveRun,
695
+ run: CursorLiveRun,
474
696
  signal?: AbortSignal,
475
697
  ): Promise<void> {
476
- const turn: CursorNativeTurnState = {
698
+ const turn: CursorLiveTurnState = {
477
699
  stream,
478
700
  partial,
479
701
  thinkingContentIndex: -1,
@@ -484,14 +706,23 @@ async function emitCursorNativeRunNextTurn(
484
706
  while (run.pendingEvents.length > 0) {
485
707
  const event = run.pendingEvents[0];
486
708
  if (event.type === "tool") {
487
- await settleCursorNativeToolBatch(run);
709
+ await settleCursorLiveToolBatch(run);
710
+ if (signal?.aborted) throw new CursorAbortError();
488
711
  const outputText = closeCursorNativeTurnBlocks(turn);
489
712
  const tools = collectCursorNativeToolBatch(run);
490
713
  emitCursorNativeToolUseTurn(stream, partial, run, tools, outputText);
491
714
  return;
492
715
  }
716
+ if (event.type === "bridge-tool") {
717
+ await settleCursorLiveToolBatch(run);
718
+ if (signal?.aborted) throw new CursorAbortError();
719
+ const outputText = closeCursorNativeTurnBlocks(turn);
720
+ const requests = collectCursorBridgeToolBatch(run);
721
+ emitCursorBridgeToolUseTurn(stream, partial, run, requests, outputText);
722
+ return;
723
+ }
493
724
  run.pendingEvents.shift();
494
- emitCursorNativeQueuedEvent(turn, event);
725
+ emitCursorLiveQueuedEvent(turn, event, run);
495
726
  }
496
727
 
497
728
  if (run.cancelled) {
@@ -509,8 +740,9 @@ async function emitCursorNativeRunNextTurn(
509
740
  }
510
741
  if (run.done) {
511
742
  let outputText = closeCursorNativeTurnBlocks(turn);
512
- if (!outputText) {
513
- outputText += await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(run.finalText ?? run.textDeltas.join("")));
743
+ const finalText = trimAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), run.emittedText);
744
+ if (finalText) {
745
+ outputText += await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
514
746
  }
515
747
  setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
516
748
  partial.stopReason = "stop";
@@ -523,17 +755,16 @@ async function emitCursorNativeRunNextTurn(
523
755
  }
524
756
  }
525
757
 
526
- async function replayPendingCursorNativeRun(
758
+ async function replayPendingCursorLiveRun(
527
759
  stream: AssistantMessageEventStream,
528
760
  partial: AssistantMessage,
529
761
  context: Context,
530
762
  signal?: AbortSignal,
531
763
  ): Promise<boolean> {
532
- const replayId = getPendingCursorNativeReplayId(context);
533
- if (!replayId) return false;
534
- const run = pendingCursorNativeRuns.get(replayId);
764
+ const run = getPendingCursorLiveRun(context);
535
765
  if (!run) return false;
536
766
  clearCursorNativeRunIdleDispose(run);
767
+ run.bridgeRun?.resolveToolResultsFromContext(context);
537
768
  try {
538
769
  await emitCursorNativeRunNextTurn(stream, partial, run, signal);
539
770
  } catch (error) {
@@ -553,10 +784,15 @@ export function streamCursor(
553
784
  (async () => {
554
785
  const partial = makeInitialMessage(model);
555
786
  let agent: SDKAgent | null = null;
556
- let activeNativeRun: CursorNativeLiveRun | undefined;
787
+ let activeLiveRun: CursorLiveRun | undefined;
788
+ let bridgeRun: CursorPiToolBridgeRun | undefined;
789
+ let bridgeRunOwnedByLiveRun = false;
790
+ let liveRunForBridgeQueue: CursorLiveRun | undefined;
791
+ const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
557
792
  let resolvedApiKey: string | undefined;
558
793
  let abortSignal: AbortSignal | undefined;
559
794
  let abortListener: (() => void) | undefined;
795
+ let restoreCursorSdkOutputFilter: (() => void) | undefined;
560
796
 
561
797
  try {
562
798
  const throwIfAborted = (): void => {
@@ -566,7 +802,7 @@ export function streamCursor(
566
802
  stream.push({ type: "start", partial });
567
803
  throwIfAborted();
568
804
 
569
- if (await replayPendingCursorNativeRun(stream, partial, context, options?.signal)) {
805
+ if (await replayPendingCursorLiveRun(stream, partial, context, options?.signal)) {
570
806
  stream.end();
571
807
  return;
572
808
  }
@@ -575,18 +811,40 @@ export function streamCursor(
575
811
  if (!apiKey) throw new Error(MISSING_API_KEY_MESSAGE);
576
812
  resolvedApiKey = apiKey;
577
813
 
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();
814
+ // pi-ai Context/SimpleStreamOptions do not expose ExtensionContext.cwd; bridge via session_start
815
+ // until pi threads session cwd into streamSimple (cwd can change without a new session event).
816
+ const cwd = getCursorSessionCwd();
581
817
  const fastEnabled = getEffectiveFastForModelId(model.id);
582
818
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
583
819
  const settingSources = resolveCursorSettingSources();
584
820
 
585
- agent = await Agent.create({
586
- apiKey,
587
- model: selection,
588
- local: settingSources ? { cwd, settingSources } : { cwd },
589
- });
821
+ const registeredBridge = getRegisteredCursorPiToolBridge();
822
+ bridgeRun = registeredBridge
823
+ ? await registeredBridge.createRun({
824
+ onToolRequest: (request) => {
825
+ if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
826
+ queueCursorNativeEvent(liveRunForBridgeQueue, { type: "bridge-tool", request });
827
+ } else {
828
+ queuedBridgeRequestsBeforeLiveRun.push(request);
829
+ }
830
+ },
831
+ })
832
+ : undefined;
833
+ if (!bridgeRun?.enabled || !bridgeRun.mcpServers) {
834
+ await bridgeRun?.dispose();
835
+ bridgeRun = undefined;
836
+ }
837
+
838
+ installCursorMcpToolTimeoutOverride();
839
+ restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
840
+ agent = await suppressCursorSdkOutput(() =>
841
+ Agent.create({
842
+ apiKey,
843
+ model: selection,
844
+ local: settingSources ? { cwd, settingSources } : { cwd },
845
+ ...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
846
+ }),
847
+ );
590
848
  throwIfAborted();
591
849
 
592
850
  const prompt = buildCursorPrompt(context, {
@@ -604,14 +862,17 @@ export function streamCursor(
604
862
  const nativeReplayId = createCursorNativeReplayId();
605
863
  const textDeltas: string[] = [];
606
864
  let nativeToolReplayStarted = false;
607
- const liveRun: CursorNativeLiveRun | undefined = useNativeToolReplay
865
+ const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
866
+ const liveRun: CursorLiveRun | undefined = useLiveRun
608
867
  ? {
609
- id: nativeReplayId,
868
+ id: useNativeToolReplay ? nativeReplayId : bridgeRun!.id,
610
869
  agent,
870
+ bridgeRun,
611
871
  promptInputTokens,
612
872
  promptInputTokensReported: false,
613
873
  pendingEvents: [],
614
874
  textDeltas,
875
+ emittedText: "",
615
876
  recordedToolDisplayIds: [],
616
877
  done: false,
617
878
  cancelled: false,
@@ -620,11 +881,21 @@ export function streamCursor(
620
881
  }
621
882
  : undefined;
622
883
  if (liveRun) {
623
- pendingCursorNativeRuns.set(liveRun.id, liveRun);
624
- activeNativeRun = liveRun;
884
+ pendingCursorLiveRuns.set(liveRun.id, liveRun);
885
+ activeLiveRun = liveRun;
886
+ liveRunForBridgeQueue = liveRun;
887
+ bridgeRunOwnedByLiveRun = bridgeRun !== undefined;
888
+ for (const request of queuedBridgeRequestsBeforeLiveRun.splice(0)) {
889
+ queueCursorNativeEvent(liveRun, { type: "bridge-tool", request });
890
+ }
625
891
  }
626
892
  const startedToolCalls = new Map<string, unknown>();
893
+ const bridgeStartedToolCallIds = new Set<string>();
894
+ const activeShellCallIds = new Set<string>();
895
+ const ambiguousShellOutputCallIds = new Set<string>();
896
+ const shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
627
897
  const completedToolIdentities = new Set<string>();
898
+ let cursorPlanTextCandidate: string | undefined;
628
899
  const completedStartedToolFingerprints = new Set<string>();
629
900
  const completedFallbackToolFingerprints = new Set<string>();
630
901
 
@@ -684,6 +955,16 @@ export function streamCursor(
684
955
  closeTraceBlock();
685
956
  };
686
957
 
958
+ const emitCursorToolTrace = (text: string): void => {
959
+ const traceText = text.endsWith("\n") ? text : `${text}\n`;
960
+ if (liveRun) {
961
+ queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: traceText });
962
+ queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
963
+ return;
964
+ }
965
+ appendTraceBlock(traceText);
966
+ };
967
+
687
968
  const closeTraceBlock = (): void => {
688
969
  if (thinkingContentIndex < 0) return;
689
970
  const block = partial.content[thinkingContentIndex];
@@ -720,10 +1001,80 @@ export function streamCursor(
720
1001
  }
721
1002
  };
722
1003
 
1004
+ const getStartedToolCallFingerprint = (toolCall: unknown): string => {
1005
+ return getToolFingerprint({ toolName: getCursorToolName(toolCall), args: getObjectField(toolCall, "args") });
1006
+ };
1007
+
1008
+ const clearStartedToolCall = (callId: string): void => {
1009
+ startedToolCalls.delete(callId);
1010
+ bridgeStartedToolCallIds.delete(callId);
1011
+ activeShellCallIds.delete(callId);
1012
+ ambiguousShellOutputCallIds.delete(callId);
1013
+ };
1014
+
1015
+ const takeBridgeStartedToolCallId = (callId: unknown): string | undefined => {
1016
+ if (typeof callId !== "string" || !bridgeStartedToolCallIds.has(callId)) return undefined;
1017
+ bridgeStartedToolCallIds.delete(callId);
1018
+ return callId;
1019
+ };
1020
+
1021
+ const takeShellOutputDeltas = (callId: string): CursorShellOutputDeltas | undefined => {
1022
+ const deltas = shellOutputDeltasByCallId.get(callId);
1023
+ shellOutputDeltasByCallId.delete(callId);
1024
+ return deltas;
1025
+ };
1026
+
1027
+ const appendShellOutputDelta = (delta: CursorShellOutputDelta): void => {
1028
+ if (activeShellCallIds.size !== 1) {
1029
+ for (const activeCallId of activeShellCallIds) {
1030
+ ambiguousShellOutputCallIds.add(activeCallId);
1031
+ shellOutputDeltasByCallId.delete(activeCallId);
1032
+ }
1033
+ return;
1034
+ }
1035
+ const [callId] = activeShellCallIds;
1036
+ if (!callId || ambiguousShellOutputCallIds.has(callId)) return;
1037
+ let deltas = shellOutputDeltasByCallId.get(callId);
1038
+ if (!deltas) {
1039
+ deltas = { stdout: [], stderr: [] };
1040
+ shellOutputDeltasByCallId.set(callId, deltas);
1041
+ }
1042
+ deltas[delta.stream].push(delta.data);
1043
+ };
1044
+
1045
+ const removeStartedToolCallForStep = (toolCall: unknown, stepId: unknown): string | undefined => {
1046
+ if (typeof stepId === "string" && startedToolCalls.has(stepId)) {
1047
+ clearStartedToolCall(stepId);
1048
+ return stepId;
1049
+ }
1050
+ const fingerprint = getStartedToolCallFingerprint(toolCall);
1051
+ for (const [callId, startedToolCall] of startedToolCalls) {
1052
+ if (getStartedToolCallFingerprint(startedToolCall) !== fingerprint) continue;
1053
+ clearStartedToolCall(callId);
1054
+ return callId;
1055
+ }
1056
+ return undefined;
1057
+ };
1058
+
1059
+ const discardIncompleteStartedToolCalls = (): void => {
1060
+ startedToolCalls.clear();
1061
+ bridgeStartedToolCallIds.clear();
1062
+ activeShellCallIds.clear();
1063
+ ambiguousShellOutputCallIds.clear();
1064
+ shellOutputDeltasByCallId.clear();
1065
+ };
1066
+
723
1067
  const handleCompletedToolCall = (
724
1068
  toolCall: unknown,
725
1069
  options: { identity?: string; source?: "started" | "fallback" } = {},
726
1070
  ): void => {
1071
+ const planText = getCursorCreatePlanText(toolCall);
1072
+ if (planText) cursorPlanTextCandidate = scrubSensitiveText(planText, resolvedApiKey);
1073
+
1074
+ if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
1075
+ if (options.identity) completedToolIdentities.add(options.identity);
1076
+ return;
1077
+ }
727
1078
  const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
728
1079
  const display = buildCursorPiToolDisplay(toolCall, { cwd });
729
1080
  const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
@@ -740,11 +1091,10 @@ export function streamCursor(
740
1091
  completedFallbackToolFingerprints.add(fingerprint);
741
1092
  }
742
1093
 
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
- }
1094
+ const nativeRenderable = canRenderCursorToolNatively(display.toolName);
1095
+ const route = useNativeToolReplay && nativeRenderable && liveRun ? "native_replay" : "trace";
1096
+
1097
+ if (route === "native_replay" && liveRun) {
748
1098
  nativeToolReplayStarted = true;
749
1099
  const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
750
1100
  queueCursorNativeEvent(liveRun, {
@@ -759,7 +1109,7 @@ export function streamCursor(
759
1109
  return;
760
1110
  }
761
1111
 
762
- appendTraceBlock(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
1112
+ emitCursorToolTrace(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
763
1113
  };
764
1114
 
765
1115
  const onDelta = (args: { update: InteractionUpdate }): void => {
@@ -767,36 +1117,50 @@ export function streamCursor(
767
1117
 
768
1118
  if (update.type === "text-delta") {
769
1119
  textDeltas.push(update.text);
770
- if (liveRun && nativeToolReplayStarted) {
1120
+ if (liveRun) {
771
1121
  queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
772
- } else if (!useNativeToolReplay) {
1122
+ } else {
773
1123
  appendLiveTextDelta(update.text);
774
1124
  }
775
1125
  } else if (update.type === "thinking-delta") {
776
- if (liveRun && nativeToolReplayStarted) {
1126
+ if (liveRun) {
777
1127
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
778
1128
  } else {
779
1129
  appendTraceDelta(update.text);
780
1130
  }
781
1131
  } else if (update.type === "thinking-completed") {
782
- if (liveRun && nativeToolReplayStarted) {
1132
+ if (liveRun) {
783
1133
  queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
784
1134
  } else {
785
1135
  closeTraceBlock();
786
1136
  }
787
1137
  } else if (update.type === "tool-call-started") {
788
- startedToolCalls.set(update.callId, update.toolCall);
1138
+ if (liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
1139
+ if (typeof update.callId === "string") bridgeStartedToolCallIds.add(update.callId);
1140
+ } else {
1141
+ startedToolCalls.set(update.callId, update.toolCall);
1142
+ if (isCursorShellToolCall(update.toolCall)) activeShellCallIds.add(update.callId);
1143
+ }
789
1144
  } else if (update.type === "tool-call-completed") {
790
- const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
791
- startedToolCalls.delete(update.callId);
792
1145
  const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
793
- handleCompletedToolCall(mergedToolCall, {
1146
+ const bridgeStartedCallId = takeBridgeStartedToolCallId(update.callId);
1147
+ if (bridgeStartedCallId) {
1148
+ completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
1149
+ return;
1150
+ }
1151
+ const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
1152
+ clearStartedToolCall(update.callId);
1153
+ const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(mergedToolCall, takeShellOutputDeltas(update.callId));
1154
+ handleCompletedToolCall(toolCallWithShellOutput, {
794
1155
  identity,
795
1156
  source: identity ? "started" : "fallback",
796
1157
  });
1158
+ } else if (update.type === "shell-output-delta") {
1159
+ const delta = getCursorShellOutputDelta(update);
1160
+ if (delta) appendShellOutputDelta(delta);
797
1161
  } else if (update.type === "summary") {
798
1162
  const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
799
- if (liveRun && nativeToolReplayStarted) {
1163
+ if (liveRun) {
800
1164
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
801
1165
  } else {
802
1166
  appendTraceDelta(summary);
@@ -805,24 +1169,43 @@ export function streamCursor(
805
1169
  // Cursor turn-ended usage is intentionally not copied into pi usage: the SDK reports
806
1170
  // cumulative internal agent/tool/cache tokens, not the replayable pi prompt context.
807
1171
  // partial-tool-call, summary-started, summary-completed, turn-ended,
808
- // shell-output-delta, token-delta, step-* are intentionally not surfaced.
1172
+ // token-delta, step-* are intentionally not surfaced.
809
1173
  };
810
1174
 
811
1175
  const onStep = (args: { step: unknown }): void => {
1176
+ const stepType = getObjectField(args.step, "type");
812
1177
  const step = getObjectField(args.step, "message") ? args.step : undefined;
813
- if (getObjectField(args.step, "type") !== "toolCall") return;
814
- const toolCall = getObjectField(step, "message");
1178
+ const rawStepToolCall = getObjectField(step, "message");
1179
+ if (stepType !== "toolCall") return;
1180
+ const toolCall = rawStepToolCall;
815
1181
  const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
816
1182
  if (toolCall) {
817
- handleCompletedToolCall(toolCall, {
818
- identity: typeof stepId === "string" ? `cursor-tool:${stepId}` : undefined,
1183
+ const bridgeStartedCallId = takeBridgeStartedToolCallId(stepId);
1184
+ if (bridgeStartedCallId) {
1185
+ completedToolIdentities.add(`cursor-tool:${bridgeStartedCallId}`);
1186
+ return;
1187
+ }
1188
+ const matchedStartedCallId = removeStartedToolCallForStep(toolCall, stepId);
1189
+ const toolCallWithShellOutput = mergeShellOutputDeltasIntoCursorToolCall(
1190
+ toolCall,
1191
+ matchedStartedCallId ? takeShellOutputDeltas(matchedStartedCallId) : undefined,
1192
+ );
1193
+ if (liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) {
1194
+ if (matchedStartedCallId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
1195
+ return;
1196
+ }
1197
+ const identityId = typeof stepId === "string" ? stepId : matchedStartedCallId;
1198
+ handleCompletedToolCall(toolCallWithShellOutput, {
1199
+ identity: identityId ? `cursor-tool:${identityId}` : undefined,
819
1200
  });
1201
+ if (matchedStartedCallId && matchedStartedCallId !== stepId) completedToolIdentities.add(`cursor-tool:${matchedStartedCallId}`);
820
1202
  }
821
1203
  };
822
1204
 
823
1205
  // Handle abort signal
824
1206
  let run: Awaited<ReturnType<SDKAgent["send"]>> | null = null;
825
1207
  abortListener = () => {
1208
+ activeLiveRun?.bridgeRun?.cancel("Cursor SDK run aborted");
826
1209
  if (run) {
827
1210
  run.cancel().catch(() => {});
828
1211
  }
@@ -840,15 +1223,16 @@ export function streamCursor(
840
1223
  throw new CursorAbortError();
841
1224
  }
842
1225
 
843
- if (useNativeToolReplay && liveRun) {
1226
+ if (liveRun) {
844
1227
  void run
845
1228
  .wait()
846
1229
  .then(async (result) => {
847
1230
  if (liveRun.disposed) return;
1231
+ discardIncompleteStartedToolCalls();
848
1232
  await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
849
1233
  if (liveRun.disposed) return;
850
1234
  liveRun.cancelled = result.status === "cancelled";
851
- liveRun.finalText = hasUsableText(result.result) ? result.result : liveRun.textDeltas.join("");
1235
+ liveRun.finalText = selectCursorFinalText(result.result, liveRun.textDeltas, liveRun.emittedText, cursorPlanTextCandidate);
852
1236
  liveRun.done = true;
853
1237
  notifyCursorNativeRun(liveRun);
854
1238
  scheduleCursorNativeRunIdleDispose(liveRun);
@@ -862,7 +1246,7 @@ export function streamCursor(
862
1246
 
863
1247
  try {
864
1248
  await waitForCursorNativeRunProgress(liveRun, options?.signal);
865
- await settleCursorNativeToolBatch(liveRun);
1249
+ await settleCursorLiveToolBatch(liveRun);
866
1250
  closeTraceBlock();
867
1251
  await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
868
1252
  } catch (error) {
@@ -874,6 +1258,7 @@ export function streamCursor(
874
1258
  }
875
1259
 
876
1260
  const result = await run.wait();
1261
+ discardIncompleteStartedToolCalls();
877
1262
  await cacheSdkContextWindow(agent.agentId, model.id);
878
1263
 
879
1264
  // Close any open thinking/activity trace, then use the final run result only when
@@ -884,11 +1269,13 @@ export function streamCursor(
884
1269
  partial.stopReason = "aborted";
885
1270
  stream.push({ type: "error", reason: "aborted", error: partial });
886
1271
  } else {
887
- const finalText = flushText(textDeltas.length === 0 && hasUsableText(result.result) ? [result.result] : []);
1272
+ const finalCursorText = selectCursorFinalText(result.result, textDeltas, textDeltas.join(""), cursorPlanTextCandidate);
1273
+ const finalText = flushText(hasUsableText(finalCursorText) ? [finalCursorText] : []);
888
1274
  setApproximateUsage(partial, promptInputTokens, finalText);
889
1275
  stream.push({ type: "done", reason: "stop", message: partial });
890
1276
  }
891
1277
  } catch (error) {
1278
+ if (activeLiveRun && !activeLiveRun.disposed) await disposeCursorNativeRun(activeLiveRun);
892
1279
  if (error instanceof CursorAbortError) {
893
1280
  partial.stopReason = "aborted";
894
1281
  stream.push({ type: "error", reason: "aborted", error: partial });
@@ -898,12 +1285,21 @@ export function streamCursor(
898
1285
  stream.push({ type: "error", reason: "error", error: partial });
899
1286
  }
900
1287
  } finally {
901
- if (activeNativeRun?.disposed) agent = null;
1288
+ restoreCursorSdkOutputFilter?.();
1289
+ if (activeLiveRun?.disposed) agent = null;
902
1290
 
903
1291
  if (abortSignal && abortListener) {
904
1292
  abortSignal.removeEventListener("abort", abortListener);
905
1293
  }
906
1294
 
1295
+ if (bridgeRun && !bridgeRunOwnedByLiveRun) {
1296
+ try {
1297
+ await bridgeRun.dispose();
1298
+ } catch {
1299
+ // bridge disposal failure should not mask original error
1300
+ }
1301
+ }
1302
+
907
1303
  if (agent) {
908
1304
  try {
909
1305
  await agent[Symbol.asyncDispose]();
@@ -922,7 +1318,7 @@ export function streamCursor(
922
1318
 
923
1319
  export const __testUtils = {
924
1320
  DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
925
- pendingCursorNativeRunCount: () => pendingCursorNativeRuns.size,
1321
+ pendingCursorNativeRunCount: () => pendingCursorLiveRuns.size,
926
1322
  setCursorNativeReplayIdleDisposeMs: (value: number) => {
927
1323
  cursorNativeReplayIdleDisposeMs = value;
928
1324
  },