import type { AssistantMessage } from "@earendil-works/pi-ai"; import { cursorLiveRuns } from "./provider-live-run-drain.js"; import type { CursorRunOutcome } from "./provider-run-outcome.js"; import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause, sanitizeCursorProviderError, } from "./provider-errors.js"; import { CursorLiveRunAbortError } from "./live-run-coordinator.js"; import { buildIncompleteCursorToolRunOutcome, type IncompleteCursorToolRunOutcomeInput, } from "./incomplete-tool-visibility.js"; import type { installCursorSdkProcessErrorGuard } from "./sdk-process-error-guard.js"; import type { CursorSdkEventDebugSink } from "./sdk-event-debug.js"; import { awaitFinalizeCursorRunOutcome } from "./provider-turn-finalize.js"; import type { CursorProviderTurnPrepareResult, CursorProviderTurnRunnerParams, CursorProviderTurnSend, CursorProviderTurnSendResult, LiveCursorProviderTurnRuntime, } from "./provider-turn-types.js"; import { applyCursorUsage } from "./usage-accounting.js"; import { hasUsableText } from "./record-utils.js"; export type CursorTurnTerminalEvent = | { kind: "direct"; prepared: CursorProviderTurnPrepareResult; outcome: CursorRunOutcome; } | { kind: "error"; prepared: CursorProviderTurnPrepareResult | undefined; error: unknown }; function applyLiveRunOutcome( outcome: CursorRunOutcome, prepared: CursorProviderTurnPrepareResult & { runtime: LiveCursorProviderTurnRuntime }, context: CursorProviderTurnRunnerParams["context"], ): void { if (prepared.runtime.liveRun.disposed) return; const { liveRun } = prepared.runtime; switch (outcome.kind) { case "finished": prepared.lifecycle.commitSend(context, prepared.meta.bootstrap); cursorLiveRuns.markFinished(liveRun, outcome.finalText); break; case "cancelled": cursorLiveRuns.markCancelled(liveRun, outcome.abortMessage); break; case "error": cursorLiveRuns.markError(liveRun, outcome.errorMessage); break; } } export interface CursorLiveRunCompletion { waitCompletion: Promise; } export interface CursorRunFinalizerParams { runnerParams: CursorProviderTurnRunnerParams; sdkEventDebug: () => CursorSdkEventDebugSink | undefined; sdkProcessErrorGuard: ReturnType; resolvedApiKey: () => string | undefined; } export interface StartCursorLiveRunCompletionParams { send: CursorProviderTurnSend; prepared: CursorProviderTurnPrepareResult & { runtime: LiveCursorProviderTurnRuntime }; modelId: string; discardIncompleteTools: (outcome: IncompleteCursorToolRunOutcomeInput) => void; } export class CursorRunFinalizer { private terminalApplied = false; constructor(private readonly params: CursorRunFinalizerParams) {} startLiveRunCompletion(startParams: StartCursorLiveRunCompletionParams): CursorLiveRunCompletion { const { runnerParams } = this.params; const sdkEventDebug = this.params.sdkEventDebug(); const { send, prepared, modelId, discardIncompleteTools } = startParams; const { run, cursorAgentMessageOffset } = send; const { liveRun } = prepared.runtime; const waitCompletion = awaitFinalizeCursorRunOutcome({ run, prepared, cursorAgentMessageOffset, modelId, signal: runnerParams.options?.signal, runResultFallback: run.result, runErrorFallback: run.error, resolvedApiKey: this.params.resolvedApiKey(), optionsApiKey: runnerParams.options?.apiKey, sdkEventDebug, cacheContextWindow: true, }) .then(async (finalized) => { applyLiveRunOutcome(finalized.outcome, prepared, runnerParams.context); }) .catch((error: unknown) => { this.safeCleanup(() => discardIncompleteTools({ status: "error" })); if (!liveRun.disposed) { cursorLiveRuns.markError( liveRun, sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? runnerParams.options?.apiKey), ); } this.safeCleanup(() => sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) })); this.safeCleanup(() => sdkEventDebug?.recordError("run_wait", error)); }); // Mark the pooled local agent busy as soon as the SDK run exists so auto-compaction summarization // (and other concurrent acquires) wait for run.wait() instead of hitting AgentBusyError. prepared.lifecycle.trackRunCompletion(waitCompletion); return { waitCompletion }; } async applyTerminalEvent(event: CursorTurnTerminalEvent): Promise { if (this.terminalApplied) return; if (event.kind === "direct") { await this.applyDirectOutcome(event.prepared, event.outcome); this.terminalApplied = true; return; } await this.applyErrorOutcome(event.prepared, event.error); this.terminalApplied = true; } async cleanup( prepared: CursorProviderTurnPrepareResult | undefined, sendResult: CursorProviderTurnSendResult | undefined, liveCompletion: CursorLiveRunCompletion | undefined, ): Promise { this.safeCleanup(() => prepared?.restoreCursorSdkOutputFilter()); const abortRegistration = sendResult?.abortRegistration; if (abortRegistration) { this.safeCleanup(() => abortRegistration.signal.removeEventListener("abort", abortRegistration.listener)); } this.params.runnerParams.sdkEventDebugRef.current = undefined; if (liveCompletion) { void liveCompletion.waitCompletion .finally(async () => { await prepared?.lifecycle.dispose().catch(() => {}); await this.finalizeSdkEventDebugBestEffort(); this.safeCleanup(() => this.params.sdkProcessErrorGuard.dispose()); }) .catch(() => {}); return; } await prepared?.lifecycle.dispose().catch(() => {}); await this.finalizeSdkEventDebugBestEffort(); this.safeCleanup(() => this.params.sdkProcessErrorGuard.dispose()); } private async applyDirectOutcome( prepared: CursorProviderTurnPrepareResult, outcome: CursorRunOutcome, ): Promise { const { stream, partial, model, context } = this.params.runnerParams; prepared.runtime.turnCoordinator.closeTraceBlock(); switch (outcome.kind) { case "cancelled": await prepared.lifecycle.abandon(); this.pushTerminalError(partial, "aborted", outcome.abortMessage); break; case "error": await prepared.lifecycle.abandon(); this.pushTerminalError(partial, "error", outcome.errorMessage); break; case "finished": prepared.lifecycle.commitSend(context, prepared.meta.bootstrap); prepared.runtime.turnCoordinator.flushText( hasUsableText(outcome.finalText) ? [outcome.finalText] : [], ); applyCursorUsage(partial, model, context, prepared.meta.promptInputTokens, { turn: prepared.runtime.turnCoordinator.lastSdkTurnUsage, billed: prepared.runtime.billedTurnUsage, }); stream.push({ type: "done", reason: "stop", message: partial }); break; } } private async applyErrorOutcome(prepared: CursorProviderTurnPrepareResult | undefined, error: unknown): Promise { this.safeCleanup(() => prepared?.runtime.turnCoordinator.discardIncompleteStartedToolCalls( buildIncompleteCursorToolRunOutcome({ status: error instanceof CursorLiveRunAbortError ? "cancelled" : "error", signalAborted: error instanceof CursorLiveRunAbortError, }), )); const activeLiveRun = prepared?.runtime.liveRun; if (activeLiveRun && !activeLiveRun.disposed) { await cursorLiveRuns.release(activeLiveRun); } else { await prepared?.lifecycle.abandon(); } this.safeCleanup(() => this.params.sdkEventDebug()?.recordError("provider_stream", error)); if (error instanceof CursorLiveRunAbortError) { this.params.sdkProcessErrorGuard.suppressAbortErrors(); this.pushTerminalError(this.params.runnerParams.partial, "aborted", this.abortMessage()); } else { this.pushTerminalError( this.params.runnerParams.partial, "error", sanitizeCursorProviderError( error, this.params.resolvedApiKey() ?? this.params.runnerParams.options?.apiKey, ), ); } } private pushTerminalError(partial: AssistantMessage, reason: "error" | "aborted", message: string): void { partial.stopReason = reason; partial.errorMessage = message; this.params.runnerParams.stream.push({ type: "error", reason, error: partial }); } private abortMessage(): string { return formatCursorSdkAbortMessage( resolveCursorSdkAbortCause({ signalAborted: this.params.runnerParams.options?.signal?.aborted }), ); } private safeCleanup(cleanup: () => void): void { try { cleanup(); } catch { // Cleanup must not reclassify an already-emitted provider turn. } } private async finalizeSdkEventDebugBestEffort(): Promise { try { this.params.sdkEventDebug()?.recordFinalPartial(this.params.runnerParams.partial); await this.params.sdkEventDebug()?.finalize(); } catch { // Debug artifact IO is best-effort and must not emit a second terminal event. } } }