/** * CursorAdapterLive - Scoped live implementation for the Cursor ACP provider adapter. * * Spawns `agent acp` over stdio, manages JSON-RPC session lifecycle, and maps * ACP notifications/requests into canonical provider runtime events. * * @module CursorAdapterLive */import { randomUUID } from "node:crypto";import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";import readline from "node:readline"; import { ApprovalRequestId, EventId, type CanonicalItemType, type CanonicalRequestType, ProviderItemId, type ProviderRuntimeEvent, type ProviderSession, type ProviderTurnStartResult, type RuntimeMode, RuntimeItemId, RuntimeRequestId, ThreadId, TurnId,} from "@t3tools/contracts";import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect"; import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, type ProviderAdapterError,} from "../Errors.ts";import { CursorAdapter, type CursorAdapterShape, CursorAcpInitializeResult, CursorAcpPermissionRequest, CursorAcpSessionNewResult, CursorAcpSessionPromptResult, CursorAcpSessionUpdateNotification,} from "../Services/CursorAdapter.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "cursor" as const;const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;const CURSOR_ACP_PROTOCOL_VERSION = 1; interface CursorResumeState { readonly acpSessionId?: string;} interface PendingRequest { readonly method: string; readonly timeout: ReturnType<typeof setTimeout>; readonly resolve: (value: unknown) => void; readonly reject: (error: Error) => void;} interface PendingPermission { readonly jsonRpcId: string | number; readonly requestType: CanonicalRequestType; readonly options: ReadonlyArray<{ optionId: string }>;} interface CursorTurnState { readonly turnId: TurnId; readonly assistantItemId: ReturnType<typeof ProviderItemId.makeUnsafe>; readonly startedToolCalls: Set<string>; readonly toolCalls: Map<string, { itemType: CanonicalItemType; title: string }>; readonly items: Array<unknown>;} interface CursorSessionContext { session: ProviderSession; runtimeMode: RuntimeMode; readonly child: ChildProcessWithoutNullStreams; readonly output: readline.Interface; readonly pending: Map<string, PendingRequest>; readonly pendingPermissions: Map<ApprovalRequestId, PendingPermission>; readonly turns: Array<{ id: TurnId; items: Array<unknown>; }>; turnState: CursorTurnState | undefined; acpSessionId: string; nextRpcId: number; stopping: boolean;} export interface CursorAdapterLiveOptions { readonly createProcess?: (input: { readonly binaryPath: string; readonly cwd: string; readonly env: NodeJS.ProcessEnv; readonly model?: string; }) => ChildProcessWithoutNullStreams; readonly nativeEventLogPath?: string; readonly nativeEventLogger?: EventNdjsonLogger;} function toMessage(cause: unknown, fallback: string): string { if (cause instanceof Error && cause.message.length > 0) { return cause.message; } return fallback;} function asRuntimeItemId(value: string): RuntimeItemId { return RuntimeItemId.makeUnsafe(value);} function asProviderItemId(value: string): ProviderItemId { return ProviderItemId.makeUnsafe(value);} function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { return RuntimeRequestId.makeUnsafe(value);} function toSessionError( threadId: ThreadId, cause: unknown,): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { const normalized = toMessage(cause, "").toLowerCase(); if (normalized.includes("unknown session") || normalized.includes("not found")) { return new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId, cause, }); } if (normalized.includes("closed")) { return new ProviderAdapterSessionClosedError({ provider: PROVIDER, threadId, cause, }); } return undefined;} function toRequestError( threadId: ThreadId, method: string, cause: unknown,): ProviderAdapterError { const sessionError = toSessionError(threadId, cause); if (sessionError) { return sessionError; } return new ProviderAdapterRequestError({ provider: PROVIDER, method, detail: toMessage(cause, `${method} failed`), cause, });} function asObject(value: unknown): Record<string, unknown> | undefined { if (!value || typeof value !== "object") { return undefined; } return value as Record<string, unknown>;} function asString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined;} function normalizeToolItemType(kind: unknown, title: unknown): CanonicalItemType { const normalizedKind = asString(kind)?.toLowerCase(); const normalizedTitle = asString(title)?.toLowerCase(); if (normalizedKind === "execute") { return "command_execution"; } if (normalizedKind === "edit" || normalizedKind === "write") { return "file_change"; } if (normalizedKind === "mcp") { return "mcp_tool_call"; } if (normalizedTitle?.includes("terminal")) { return "command_execution"; } return "dynamic_tool_call";} function normalizeRequestType(toolCall: unknown): CanonicalRequestType { const record = asObject(toolCall); const kind = asString(record?.kind)?.toLowerCase(); if (kind === "execute") { return "command_execution_approval"; } if (kind === "edit" || kind === "write") { return "file_change_approval"; } return "unknown";} function selectCursorPermissionOption( options: ReadonlyArray<{ optionId: string }>, decision: "acceptForSession" | "accept" | "decline" | "cancel",): string | undefined { const allowAlways = options.find((option) => option.optionId === "allow-always"); const allowOnce = options.find((option) => option.optionId === "allow-once"); const rejectOnce = options.find((option) => option.optionId === "reject-once"); if (decision === "acceptForSession") { return allowAlways?.optionId ?? allowOnce?.optionId; } if (decision === "accept") { return allowOnce?.optionId ?? allowAlways?.optionId; } return rejectOnce?.optionId ?? options[0]?.optionId;} function selectCursorAutoApprovalOption( options: ReadonlyArray<{ optionId: string }>,): { optionId: string; decision: "acceptForSession" | "accept" } | undefined { const allowAlways = options.find((option) => option.optionId === "allow-always"); if (allowAlways) { return { optionId: allowAlways.optionId, decision: "acceptForSession", }; } const allowOnce = options.find((option) => option.optionId === "allow-once"); if (allowOnce) { return { optionId: allowOnce.optionId, decision: "accept", }; } return undefined;} function titleForItemType(itemType: CanonicalItemType): string { switch (itemType) { case "command_execution": return "Command run"; case "file_change": return "File change"; case "mcp_tool_call": return "MCP tool call"; case "dynamic_tool_call": return "Tool call"; default: return "Item"; }} function summarizeToolOutput(rawOutput: unknown): string | undefined { const output = asObject(rawOutput); if (!output) return undefined; const stdout = asString(output.stdout); if (stdout && stdout.trim().length > 0) { return stdout.trim().slice(0, 400); } const summary = JSON.stringify(output); return summary.length > 400 ? `${summary.slice(0, 397)}...` : summary;} function mapStopReasonToTurnState( stopReason: string | undefined,): "completed" | "failed" | "interrupted" | "cancelled" { if (stopReason === "cancelled") return "cancelled"; if (stopReason === "interrupted") return "interrupted"; return "completed";} function readCursorResumeState(resumeCursor: unknown): CursorResumeState | undefined { if (!resumeCursor || typeof resumeCursor !== "object") { return undefined; } const cursor = resumeCursor as { acpSessionId?: unknown; sessionId?: unknown; }; const acpSessionId = typeof cursor.acpSessionId === "string" ? cursor.acpSessionId : typeof cursor.sessionId === "string" ? cursor.sessionId : undefined; if (!acpSessionId) { return {}; } return { acpSessionId };} function writeCursorMessage(context: CursorSessionContext, message: unknown): void { if (!context.child.stdin.writable) { throw new Error("Cannot write to Cursor ACP stdin."); } context.child.stdin.write(`${JSON.stringify(message)}\n`);} function makeCursorAdapter(options?: CursorAdapterLiveOptions) { return Effect.gen(function* () { const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { stream: "native", }) : undefined); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id)); const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); const sessions = new Map<ThreadId, CursorSessionContext>(); const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); const spawnCursorAcp = (input: { readonly binaryPath: string; readonly cwd: string; readonly env: NodeJS.ProcessEnv; readonly model?: string; }): ChildProcessWithoutNullStreams => { if (options?.createProcess) { return options.createProcess(input); } const args = input.model ? ["--model", input.model, "acp"] : ["acp"]; return spawn(input.binaryPath, args, { cwd: input.cwd, env: input.env, stdio: ["pipe", "pipe", "pipe"], }); }; const sendRequest = ( context: CursorSessionContext, method: string, params: unknown, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, ): Promise<unknown> => { const id = context.nextRpcId; context.nextRpcId += 1; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { context.pending.delete(String(id)); reject(new Error(`Timed out waiting for ${method}.`)); }, timeoutMs); context.pending.set(String(id), { method, timeout, resolve, reject, }); writeCursorMessage(context, { jsonrpc: "2.0", id, method, params, }); }); }; const resolvePendingRequest = ( context: CursorSessionContext, message: Record<string, unknown>, ) => { const id = message.id; if (typeof id !== "string" && typeof id !== "number") { return; } const pending = context.pending.get(String(id)); if (!pending) { return; } clearTimeout(pending.timeout); context.pending.delete(String(id)); const error = asObject(message.error); if (error) { pending.reject(new Error(`${pending.method} failed: ${JSON.stringify(error)}`)); return; } pending.resolve(message.result); }; const decodePermissionRequest = Schema.decodeUnknownSync(CursorAcpPermissionRequest); const decodeSessionUpdateNotification = Schema.decodeUnknownSync( CursorAcpSessionUpdateNotification, ); const emitRuntimeWarning = ( context: CursorSessionContext, message: string, detail?: unknown, ): Effect.Effect<void> => Effect.gen(function* () { const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "runtime.warning", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...((context.turnState ? { turnId: context.turnState.turnId } : {})), payload: { message, ...(detail !== undefined ? { detail } : {}), }, ...(context.turnState ? { providerRefs: { providerTurnId: String(context.turnState.turnId) } } : {}), }); }); const completeTurn = ( context: CursorSessionContext, state: "completed" | "failed" | "interrupted" | "cancelled", errorMessage?: string, stopReason?: string, ): Effect.Effect<void> => Effect.gen(function* () { const turnState = context.turnState; if (!turnState) { return; } const itemStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.completed", eventId: itemStamp.eventId, provider: PROVIDER, createdAt: itemStamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, itemId: asRuntimeItemId(turnState.assistantItemId), payload: { itemType: "assistant_message", status: "completed", title: "Assistant message", }, providerRefs: { providerTurnId: String(turnState.turnId), providerItemId: turnState.assistantItemId, }, }); context.turns.push({ id: turnState.turnId, items: [...turnState.items], }); const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.completed", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, payload: { state, ...(stopReason ? { stopReason } : {}), ...(errorMessage ? { errorMessage } : {}), }, providerRefs: { providerTurnId: String(turnState.turnId), }, }); context.turnState = undefined; context.session = { ...context.session, status: state === "failed" ? "error" : "ready", activeTurnId: undefined, ...(errorMessage ? { lastError: errorMessage } : {}), updatedAt: yield* nowIso, }; }); const handlePermissionRequest = ( context: CursorSessionContext, request: unknown, ): Effect.Effect<void> => { let decoded: ReturnType<typeof decodePermissionRequest>;Lines 1–500 of 1566. Load more
/** * CursorAdapterLive - Scoped live implementation for the Cursor ACP provider adapter. * * Spawns `agent acp` over stdio, manages JSON-RPC session lifecycle, and maps * ACP notifications/requests into canonical provider runtime events. * * @module CursorAdapterLive */ import { randomUUID } from "node:crypto"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import readline from "node:readline"; import { ApprovalRequestId, EventId, type CanonicalItemType, type CanonicalRequestType, ProviderItemId, type ProviderRuntimeEvent, type ProviderSession, type ProviderTurnStartResult, type RuntimeMode, RuntimeItemId, RuntimeRequestId, ThreadId, TurnId, } from "@t3tools/contracts"; import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect"; import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, type ProviderAdapterError, } from "../Errors.ts"; import { CursorAdapter, type CursorAdapterShape, CursorAcpInitializeResult, CursorAcpPermissionRequest, CursorAcpSessionNewResult, CursorAcpSessionPromptResult, CursorAcpSessionUpdateNotification, } from "../Services/CursorAdapter.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "cursor" as const; const DEFAULT_REQUEST_TIMEOUT_MS = 120_000; const CURSOR_ACP_PROTOCOL_VERSION = 1; interface CursorResumeState { readonly acpSessionId?: string; } interface PendingRequest { readonly method: string; readonly timeout: ReturnType<typeof setTimeout>; readonly resolve: (value: unknown) => void; readonly reject: (error: Error) => void; } interface PendingPermission { readonly jsonRpcId: string | number; readonly requestType: CanonicalRequestType; readonly options: ReadonlyArray<{ optionId: string }>; } interface CursorTurnState { readonly turnId: TurnId; readonly assistantItemId: ReturnType<typeof ProviderItemId.makeUnsafe>; readonly startedToolCalls: Set<string>; readonly toolCalls: Map<string, { itemType: CanonicalItemType; title: string }>; readonly items: Array<unknown>; } interface CursorSessionContext { session: ProviderSession; runtimeMode: RuntimeMode; readonly child: ChildProcessWithoutNullStreams; readonly output: readline.Interface; readonly pending: Map<string, PendingRequest>; readonly pendingPermissions: Map<ApprovalRequestId, PendingPermission>; readonly turns: Array<{ id: TurnId; items: Array<unknown>; }>; turnState: CursorTurnState | undefined; acpSessionId: string; nextRpcId: number; stopping: boolean; } export interface CursorAdapterLiveOptions { readonly createProcess?: (input: { readonly binaryPath: string; readonly cwd: string; readonly env: NodeJS.ProcessEnv; readonly model?: string; }) => ChildProcessWithoutNullStreams; readonly nativeEventLogPath?: string; readonly nativeEventLogger?: EventNdjsonLogger; } function toMessage(cause: unknown, fallback: string): string { if (cause instanceof Error && cause.message.length > 0) { return cause.message; } return fallback; } function asRuntimeItemId(value: string): RuntimeItemId { return RuntimeItemId.makeUnsafe(value); } function asProviderItemId(value: string): ProviderItemId { return ProviderItemId.makeUnsafe(value); } function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { return RuntimeRequestId.makeUnsafe(value); } function toSessionError( threadId: ThreadId, cause: unknown, ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { const normalized = toMessage(cause, "").toLowerCase(); if (normalized.includes("unknown session") || normalized.includes("not found")) { return new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId, cause, }); } if (normalized.includes("closed")) { return new ProviderAdapterSessionClosedError({ provider: PROVIDER, threadId, cause, }); } return undefined; } function toRequestError( threadId: ThreadId, method: string, cause: unknown, ): ProviderAdapterError { const sessionError = toSessionError(threadId, cause); if (sessionError) { return sessionError; } return new ProviderAdapterRequestError({ provider: PROVIDER, method, detail: toMessage(cause, `${method} failed`), cause, }); } function asObject(value: unknown): Record<string, unknown> | undefined { if (!value || typeof value !== "object") { return undefined; } return value as Record<string, unknown>; } function asString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } function normalizeToolItemType(kind: unknown, title: unknown): CanonicalItemType { const normalizedKind = asString(kind)?.toLowerCase(); const normalizedTitle = asString(title)?.toLowerCase(); if (normalizedKind === "execute") { return "command_execution"; } if (normalizedKind === "edit" || normalizedKind === "write") { return "file_change"; } if (normalizedKind === "mcp") { return "mcp_tool_call"; } if (normalizedTitle?.includes("terminal")) { return "command_execution"; } return "dynamic_tool_call"; } function normalizeRequestType(toolCall: unknown): CanonicalRequestType { const record = asObject(toolCall); const kind = asString(record?.kind)?.toLowerCase(); if (kind === "execute") { return "command_execution_approval"; } if (kind === "edit" || kind === "write") { return "file_change_approval"; } return "unknown"; } function selectCursorPermissionOption( options: ReadonlyArray<{ optionId: string }>, decision: "acceptForSession" | "accept" | "decline" | "cancel", ): string | undefined { const allowAlways = options.find((option) => option.optionId === "allow-always"); const allowOnce = options.find((option) => option.optionId === "allow-once"); const rejectOnce = options.find((option) => option.optionId === "reject-once"); if (decision === "acceptForSession") { return allowAlways?.optionId ?? allowOnce?.optionId; } if (decision === "accept") { return allowOnce?.optionId ?? allowAlways?.optionId; } return rejectOnce?.optionId ?? options[0]?.optionId; } function selectCursorAutoApprovalOption( options: ReadonlyArray<{ optionId: string }>, ): { optionId: string; decision: "acceptForSession" | "accept" } | undefined { const allowAlways = options.find((option) => option.optionId === "allow-always"); if (allowAlways) { return { optionId: allowAlways.optionId, decision: "acceptForSession", }; } const allowOnce = options.find((option) => option.optionId === "allow-once"); if (allowOnce) { return { optionId: allowOnce.optionId, decision: "accept", }; } return undefined; } function titleForItemType(itemType: CanonicalItemType): string { switch (itemType) { case "command_execution": return "Command run"; case "file_change": return "File change"; case "mcp_tool_call": return "MCP tool call"; case "dynamic_tool_call": return "Tool call"; default: return "Item"; } } function summarizeToolOutput(rawOutput: unknown): string | undefined { const output = asObject(rawOutput); if (!output) return undefined; const stdout = asString(output.stdout); if (stdout && stdout.trim().length > 0) { return stdout.trim().slice(0, 400); } const summary = JSON.stringify(output); return summary.length > 400 ? `${summary.slice(0, 397)}...` : summary; } function mapStopReasonToTurnState( stopReason: string | undefined, ): "completed" | "failed" | "interrupted" | "cancelled" { if (stopReason === "cancelled") return "cancelled"; if (stopReason === "interrupted") return "interrupted"; return "completed"; } function readCursorResumeState(resumeCursor: unknown): CursorResumeState | undefined { if (!resumeCursor || typeof resumeCursor !== "object") { return undefined; } const cursor = resumeCursor as { acpSessionId?: unknown; sessionId?: unknown; }; const acpSessionId = typeof cursor.acpSessionId === "string" ? cursor.acpSessionId : typeof cursor.sessionId === "string" ? cursor.sessionId : undefined; if (!acpSessionId) { return {}; } return { acpSessionId }; } function writeCursorMessage(context: CursorSessionContext, message: unknown): void { if (!context.child.stdin.writable) { throw new Error("Cannot write to Cursor ACP stdin."); } context.child.stdin.write(`${JSON.stringify(message)}\n`); } function makeCursorAdapter(options?: CursorAdapterLiveOptions) { return Effect.gen(function* () { const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { stream: "native", }) : undefined); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id)); const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); const sessions = new Map<ThreadId, CursorSessionContext>(); const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); const spawnCursorAcp = (input: { readonly binaryPath: string; readonly cwd: string; readonly env: NodeJS.ProcessEnv; readonly model?: string; }): ChildProcessWithoutNullStreams => { if (options?.createProcess) { return options.createProcess(input); } const args = input.model ? ["--model", input.model, "acp"] : ["acp"]; return spawn(input.binaryPath, args, { cwd: input.cwd, env: input.env, stdio: ["pipe", "pipe", "pipe"], }); }; const sendRequest = ( context: CursorSessionContext, method: string, params: unknown, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, ): Promise<unknown> => { const id = context.nextRpcId; context.nextRpcId += 1; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { context.pending.delete(String(id)); reject(new Error(`Timed out waiting for ${method}.`)); }, timeoutMs); context.pending.set(String(id), { method, timeout, resolve, reject, }); writeCursorMessage(context, { jsonrpc: "2.0", id, method, params, }); }); }; const resolvePendingRequest = ( context: CursorSessionContext, message: Record<string, unknown>, ) => { const id = message.id; if (typeof id !== "string" && typeof id !== "number") { return; } const pending = context.pending.get(String(id)); if (!pending) { return; } clearTimeout(pending.timeout); context.pending.delete(String(id)); const error = asObject(message.error); if (error) { pending.reject(new Error(`${pending.method} failed: ${JSON.stringify(error)}`)); return; } pending.resolve(message.result); }; const decodePermissionRequest = Schema.decodeUnknownSync(CursorAcpPermissionRequest); const decodeSessionUpdateNotification = Schema.decodeUnknownSync( CursorAcpSessionUpdateNotification, ); const emitRuntimeWarning = ( context: CursorSessionContext, message: string, detail?: unknown, ): Effect.Effect<void> => Effect.gen(function* () { const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "runtime.warning", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...((context.turnState ? { turnId: context.turnState.turnId } : {})), payload: { message, ...(detail !== undefined ? { detail } : {}), }, ...(context.turnState ? { providerRefs: { providerTurnId: String(context.turnState.turnId) } } : {}), }); }); const completeTurn = ( context: CursorSessionContext, state: "completed" | "failed" | "interrupted" | "cancelled", errorMessage?: string, stopReason?: string, ): Effect.Effect<void> => Effect.gen(function* () { const turnState = context.turnState; if (!turnState) { return; } const itemStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.completed", eventId: itemStamp.eventId, provider: PROVIDER, createdAt: itemStamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, itemId: asRuntimeItemId(turnState.assistantItemId), payload: { itemType: "assistant_message", status: "completed", title: "Assistant message", }, providerRefs: { providerTurnId: String(turnState.turnId), providerItemId: turnState.assistantItemId, }, }); context.turns.push({ id: turnState.turnId, items: [...turnState.items], }); const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.completed", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, payload: { state, ...(stopReason ? { stopReason } : {}), ...(errorMessage ? { errorMessage } : {}), }, providerRefs: { providerTurnId: String(turnState.turnId), }, }); context.turnState = undefined; context.session = { ...context.session, status: state === "failed" ? "error" : "ready", activeTurnId: undefined, ...(errorMessage ? { lastError: errorMessage } : {}), updatedAt: yield* nowIso, }; }); const handlePermissionRequest = ( context: CursorSessionContext, request: unknown, ): Effect.Effect<void> => { let decoded: ReturnType<typeof decodePermissionRequest>;