1import type {
2 AgentMessage,
3 AgentOptions,
4 InteractionUpdate,
5 RunResult,
6 SDKUserMessage,
7 SendOptions,
8} from "@cursor/sdk";
9import {
10 type OrchestrationV2ProviderSession,
11 ProviderDriverKind,
12 type ThreadId,
13} from "@t3tools/contracts";
14import * as Context from "effect/Context";
15import * as Deferred from "effect/Deferred";
16import * as Effect from "effect/Effect";
17import * as Layer from "effect/Layer";
18import * as Ref from "effect/Ref";
19import * as Schema from "effect/Schema";
20
21import * as CursorSdk from "./CursorSdk.ts";
22import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
23
24export const CURSOR_AGENT_SDK_PROTOCOL = "cursor-agent-sdk.local" as const;
25export const CURSOR_PROVIDER = ProviderDriverKind.make("cursor");
26
27export class CursorAgentSdkRunnerError extends Schema.TaggedError<CursorAgentSdkRunnerError>()(
28 "CursorAgentSdkRunnerError",
29 {
30 method: Schema.String,
31 cause: Schema.Defect(),
32 },
33) {
34 override get message(): string {
35 return `Cursor Agent SDK ${this.method} failed.`;
36 }
37}
38
39const isCursorAgentSdkRunnerError = Schema.is(CursorAgentSdkRunnerError);
40
41export interface CursorAgentSdkOpenInput {
42 readonly operation: "create" | "resume";
43 readonly agentId?: string;
44 readonly options: AgentOptions;
45 readonly threadId: ThreadId;
46 readonly providerSessionId: OrchestrationV2ProviderSession["id"];
47}
48
49export interface CursorAgentSdkSendInput<Error> {
50 readonly message: string | SDKUserMessage;
51 readonly options?: Omit<SendOptions, "onDelta">;
52 readonly onDelta?: (update: InteractionUpdate) => Effect.Effect<void, Error>;
53}
54
55export interface CursorAgentSdkRun {
56 readonly runId: string;
57 readonly agentId: string;
58 readonly wait: Effect.Effect<RunResult, CursorAgentSdkRunnerError>;
59 readonly cancel: Effect.Effect<void, CursorAgentSdkRunnerError>;
60}
61
62export interface CursorAgentSdkSession {
63 readonly agentId: string;
64 readonly send: <Error>(
65 input: CursorAgentSdkSendInput<Error>,
66 ) => Effect.Effect<CursorAgentSdkRun, CursorAgentSdkRunnerError>;
67 readonly listMessages: Effect.Effect<ReadonlyArray<AgentMessage>, CursorAgentSdkRunnerError>;
68 readonly close: Effect.Effect<void, CursorAgentSdkRunnerError>;
69}
70
71export class CursorAgentSdkRunner extends Context.Service<
72 CursorAgentSdkRunner,
73 {
74 readonly open: (
75 input: CursorAgentSdkOpenInput,
76 ) => Effect.Effect<CursorAgentSdkSession, CursorAgentSdkRunnerError>;
77 readonly assertComplete: Effect.Effect<void, CursorAgentSdkRunnerError>;
78 }
79>()("@t3tools/provider-cursor/server/CursorAgentSdk/CursorAgentSdkRunner") {}
80
81export interface CursorAgentSdkLoggedAgentOptions {
82 readonly model?: AgentOptions["model"];
83 readonly hasName?: boolean;
84 readonly mode?: AgentOptions["mode"];
85 readonly local?: {
86 readonly hasCwd?: boolean;
87 readonly autoReview?: boolean;
88 readonly settingSources?: AgentOptions["local"] extends infer Local
89 ? Local extends { readonly settingSources?: infer Sources }
90 ? Sources
91 : never
92 : never;
93 readonly sandboxEnabled?: boolean;
94 readonly enableAgentRetries?: boolean;
95 readonly hasCustomTools?: boolean;
96 };
97 readonly agents?: ReadonlyArray<string>;
98}
99
100export interface CursorAgentSdkLoggedSendOptions {
101 readonly model?: SendOptions["model"];
102 readonly mode?: SendOptions["mode"];
103 readonly local?: {
104 readonly force?: boolean;
105 readonly hasCustomTools?: boolean;
106 };
107 readonly idempotencyKey?: string;
108}
109
110export type CursorAgentSdkProtocolLogEvent =
111 | {
112 readonly direction: "outgoing";
113 readonly stage: "decoded";
114 readonly payload: {
115 readonly type: "agent.open";
116 readonly operation: CursorAgentSdkOpenInput["operation"];
117 readonly agentId?: string;
118 readonly options: CursorAgentSdkLoggedAgentOptions;
119 };
120 }
121 | {
122 readonly direction: "incoming";
123 readonly stage: "decoded";
124 readonly payload: {
125 readonly type: "agent.opened";
126 readonly agentId: string;
127 };
128 }
129 | {
130 readonly direction: "outgoing";
131 readonly stage: "decoded";
132 readonly payload: {
133 readonly type: "run.start";
134 readonly message: string | SDKUserMessage;
135 readonly options: CursorAgentSdkLoggedSendOptions;
136 };
137 }
138 | {
139 readonly direction: "incoming";
140 readonly stage: "decoded";
141 readonly payload: {
142 readonly type: "run.started";
143 readonly runId: string;
144 readonly agentId: string;
145 };
146 }
147 | {
148 readonly direction: "incoming";
149 readonly stage: "decoded";
150 readonly payload: {
151 readonly type: "interaction.update";
152 readonly runId: string;
153 readonly update: InteractionUpdate;
154 };
155 }
156 | {
157 readonly direction: "incoming";
158 readonly stage: "decoded";
159 readonly payload: {
160 readonly type: "run.completed";
161 readonly result: RunResult;
162 };
163 }
164 | {
165 readonly direction: "outgoing";
166 readonly stage: "decoded";
167 readonly payload: {
168 readonly type: "run.cancel";
169 readonly runId: string;
170 };
171 }
172 | {
173 readonly direction: "outgoing";
174 readonly stage: "decoded";
175 readonly payload: {
176 readonly type: "agent.messages.list";
177 readonly agentId: string;
178 };
179 }
180 | {
181 readonly direction: "incoming";
182 readonly stage: "decoded";
183 readonly payload: {
184 readonly type: "agent.messages";
185 readonly agentId: string;
186 readonly messages: ReadonlyArray<AgentMessage>;
187 };
188 }
189 | {
190 readonly direction: "outgoing";
191 readonly stage: "decoded";
192 readonly payload: {
193 readonly type: "agent.close";
194 readonly agentId: string;
195 };
196 };
197
198export type CursorAgentSdkProtocolLogger = (
199 event: CursorAgentSdkProtocolLogEvent,
200) => Effect.Effect<void>;
201
202function runnerError(cause: unknown, method: string): CursorAgentSdkRunnerError {
203 return isCursorAgentSdkRunnerError(cause)
204 ? cause
205 : new CursorAgentSdkRunnerError({ method, cause });
206}
207
208function isActiveRunConflict(cause: unknown): boolean {
209 return cause instanceof Error && /already has active run/i.test(cause.message);
210}
211
212export function isCursorCancellationError(cause: unknown): boolean {
213 let current = cause;
214 const seen = new Set<object>();
215
216 while (typeof current === "object" && current !== null && !seen.has(current)) {
217 if (Reflect.get(current, "name") === "AbortError") {
218 return true;
219 }
220 seen.add(current);
221 current = Reflect.get(current, "cause");
222 }
223
224 return false;
225}
226
227export function loggedCursorAgentOptions(options: AgentOptions): CursorAgentSdkLoggedAgentOptions {
228 return {
229 ...(options.model === undefined ? {} : { model: options.model }),
230 ...(options.name === undefined ? {} : { hasName: true }),
231 ...(options.mode === undefined ? {} : { mode: options.mode }),
232 ...(options.local === undefined
233 ? {}
234 : {
235 local: {
236 ...(options.local.cwd === undefined ? {} : { hasCwd: true }),
237 ...(options.local.autoReview === undefined
238 ? {}
239 : { autoReview: options.local.autoReview }),
240 ...(options.local.settingSources === undefined
241 ? {}
242 : { settingSources: options.local.settingSources }),
243 ...(options.local.sandboxOptions === undefined
244 ? {}
245 : { sandboxEnabled: options.local.sandboxOptions.enabled }),
246 ...(options.local.enableAgentRetries === undefined
247 ? {}
248 : { enableAgentRetries: options.local.enableAgentRetries }),
249 ...(options.local.customTools === undefined ? {} : { hasCustomTools: true }),
250 },
251 }),
252 ...(options.agents === undefined ? {} : { agents: Object.keys(options.agents).toSorted() }),
253 };
254}
255
256export function loggedCursorSendOptions(
257 options: Omit<SendOptions, "onDelta"> | undefined,
258): CursorAgentSdkLoggedSendOptions {
259 if (options === undefined) {
260 return {};
261 }
262 return {
263 ...(options.model === undefined ? {} : { model: options.model }),
264 ...(options.mode === undefined ? {} : { mode: options.mode }),
265 ...(options.local === undefined
266 ? {}
267 : {
268 local: {
269 ...(options.local.force === undefined ? {} : { force: options.local.force }),
270 ...(options.local.customTools === undefined ? {} : { hasCustomTools: true }),
271 },
272 }),
273 ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
274 };
275}
276
277function makeCursorAgentSdkProtocolLogger(input: {
278 readonly nativeEventLogger: ProviderEventLoggers.EventNdjsonLogger | undefined;
279 readonly threadId: ThreadId;
280 readonly providerSessionId: OrchestrationV2ProviderSession["id"];
281}): CursorAgentSdkProtocolLogger | undefined {
282 if (input.nativeEventLogger === undefined) {
283 return undefined;
284 }
285 const nativeEventLogger = input.nativeEventLogger;
286 return (event) =>
287 nativeEventLogger
288 .write(
289 {
290 provider: CURSOR_PROVIDER,
291 protocol: CURSOR_AGENT_SDK_PROTOCOL,
292 kind: "protocol",
293 providerSessionId: input.providerSessionId,
294 event,
295 },
296 input.threadId,
297 )
298 .pipe(Effect.ignore);
299}
300
301/**
302 * Runs agents through the Cursor SDK, logging every frame to the protocol
303 * logger chosen for each opened agent. The live layer writes the native
304 * provider event log; the replay recorder turns the same frames into a
305 * transcript.
306 */
307export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(function* (
308 protocolLoggerFor: (input: CursorAgentSdkOpenInput) => CursorAgentSdkProtocolLogger | undefined,
309) {
310 const { Agent, createAgentPlatform } = yield* CursorSdk.CursorSdk;
311
312 /**
313 * The Cursor SDK decides once per process whether local sandboxing works, and
314 * caches the answer the first time any run starts. Only sandboxed runs point
315 * it at its `cursorsandbox` helper first, so after an unsandboxed (Full access)
316 * run it caches "unsupported" and rejects every later sandboxed run until the
317 * server restarts. Warming a bare sandboxed executor before the first
318 * unsandboxed agent opens lets the SDK find the helper and cache the real
319 * answer. Warming is best effort: on a machine without sandbox support it
320 * fails, the SDK caches "unsupported", and sandboxed runs report that as
321 * before.
322 */
323 const sandboxSupportPrimeStarted = yield* Ref.make(false);
324 const sandboxSupportPrimed = yield* Deferred.make<void>();
325 const primeCursorSandboxSupport = Effect.fn("CursorAgentSdkRunner.primeSandboxSupport")(
326 function* (options: AgentOptions) {
327 if (yield* Ref.getAndSet(sandboxSupportPrimeStarted, true)) {
328 return yield* Deferred.await(sandboxSupportPrimed);
329 }
330 yield* Effect.tryPromise(async () => {
331 const cwd = typeof options.local?.cwd === "string" ? options.local.cwd : undefined;
332 const platform = await createAgentPlatform(cwd === undefined ? {} : { workspaceRef: cwd });
333 const release = await platform.prewarmLocalWorkspace({
334 ...(options.apiKey === undefined ? {} : { apiKey: options.apiKey }),
335 local: {
336 ...(cwd === undefined ? {} : { cwd }),
337 settingSources: [],
338 sandboxOptions: { enabled: true },
339 },
340 });
341 await release();
342 }).pipe(Effect.ignore, Effect.ensuring(Deferred.succeed(sandboxSupportPrimed, undefined)));
343 },
344 );
345
346 return CursorAgentSdkRunner.of({
347 open: Effect.fn("CursorAgentSdkRunner.open")(function* (input) {
348 if (input.options.local?.sandboxOptions?.enabled === false) {
349 yield* primeCursorSandboxSupport(input.options);
350 }
351 const protocolLogger = protocolLoggerFor(input);
352 const log = (event: CursorAgentSdkProtocolLogEvent) =>
353 protocolLogger === undefined ? Effect.void : protocolLogger(event);
354
355 yield* log({
356 direction: "outgoing",
357 stage: "decoded",
358 payload: {
359 type: "agent.open",
360 operation: input.operation,
361 ...(input.agentId === undefined ? {} : { agentId: input.agentId }),
362 options: loggedCursorAgentOptions(input.options),
363 },
364 });
365
366 const agent = yield* Effect.tryPromise({
367 try: () =>
368 input.operation === "create"
369 ? Agent.create(input.options)
370 : Agent.resume(input.agentId!, input.options),
371 catch: (cause) => runnerError(cause, `agent.${input.operation}`),
372 });
373
374 yield* log({
375 direction: "incoming",
376 stage: "decoded",
377 payload: {
378 type: "agent.opened",
379 agentId: agent.agentId,
380 },
381 });
382
383 const cwd =
384 typeof input.options.local?.cwd === "string"
385 ? input.options.local.cwd
386 : input.options.local?.cwd?.[0];
387 const runOptions = {
388 runtime: "local" as const,
389 ...(cwd === undefined ? {} : { cwd }),
390 ...(input.options.local?.store === undefined ? {} : { store: input.options.local.store }),
391 };
392 let abandonedRunRecoveryAvailable = input.operation === "resume";
393
394 return {
395 agentId: agent.agentId,
396 send: Effect.fn("CursorAgentSdkSession.send")(function* (sendInput) {
397 const context = yield* Effect.context();
398 yield* log({
399 direction: "outgoing",
400 stage: "decoded",
401 payload: {
402 type: "run.start",
403 message: sendInput.message,
404 options: loggedCursorSendOptions(sendInput.options),
405 },
406 });
407
408 let callbacksReady = false;
409 const pendingUpdates: Array<InteractionUpdate> = [];
410 let callbackFailure: { readonly cause: unknown } | undefined;
411 let callbackChain = Promise.resolve();
412 let runId = "";
413 const dispatchUpdate = (update: InteractionUpdate): Promise<void> => {
414 callbackChain = callbackChain
415 .then(() => {
416 if (callbackFailure !== undefined) {
417 return;
418 }
419 return Effect.runPromiseWith(context)(
420 log({
421 direction: "incoming",
422 stage: "decoded",
423 payload: {
424 type: "interaction.update",
425 runId,
426 update,
427 },
428 }).pipe(Effect.andThen(sendInput.onDelta?.(update) ?? Effect.void)),
429 );
430 })
431 .catch((cause) => {
432 callbackFailure ??= { cause };
433 });
434 return callbackChain;
435 };
436
437 const startRun = () =>
438 Effect.tryPromise({
439 try: () =>
440 agent.send(sendInput.message, {
441 ...sendInput.options,
442 onDelta: async ({ update }) => {
443 if (!callbacksReady) {
444 pendingUpdates.push(update);
445 return;
446 }
447 await dispatchUpdate(update);
448 },
449 }),
450 catch: (cause) => runnerError(cause, "run.start"),
451 });
452 const run = yield* startRun().pipe(
453 Effect.catchIf(
454 (error) => abandonedRunRecoveryAvailable && isActiveRunConflict(error.cause),
455 (error) =>
456 Effect.gen(function* () {
457 abandonedRunRecoveryAvailable = false;
458 const latestRun = yield* Effect.tryPromise({
459 try: () => Agent.listRuns(agent.agentId, { ...runOptions, limit: 1 }),
460 catch: (cause) => runnerError(cause, "agent.listRuns"),
461 });
462 const activeRun = latestRun.items.find(
463 (candidate) => candidate.status === "running",
464 );
465 if (activeRun === undefined) {
466 return yield* error;
467 }
468 yield* log({
469 direction: "outgoing",
470 stage: "decoded",
471 payload: { type: "run.cancel", runId: activeRun.id },
472 });
473 yield* Effect.tryPromise({
474 try: () => Agent.cancelRun(activeRun.id, runOptions),
475 catch: (cause) => runnerError(cause, "agent.cancelRun"),
476 });
477 return yield* startRun();
478 }),
479 ),
480 );
481 abandonedRunRecoveryAvailable = false;
482 runId = run.id;
483 yield* log({
484 direction: "incoming",
485 stage: "decoded",
486 payload: {
487 type: "run.started",
488 runId: run.id,
489 agentId: run.agentId,
490 },
491 });
492 callbacksReady = true;
493 for (const update of pendingUpdates) {
494 yield* Effect.tryPromise({
495 try: () => dispatchUpdate(update),
496 catch: (cause) => runnerError(cause, "run.onDelta"),
497 });
498 }
499
500 return {
501 runId: run.id,
502 agentId: run.agentId,
503 wait: Effect.tryPromise({
504 try: async () => {
505 const result = await run.wait();
506 await callbackChain;
507 if (callbackFailure !== undefined) {
508 throw callbackFailure.cause;
509 }
510 return result;
511 },
512 catch: (cause) => runnerError(cause, "run.wait"),
513 }).pipe(
514 Effect.tap((result) =>
515 log({
516 direction: "incoming",
517 stage: "decoded",
518 payload: {
519 type: "run.completed",
520 result,
521 },
522 }),
523 ),
524 ),
525 cancel: log({
526 direction: "outgoing",
527 stage: "decoded",
528 payload: {
529 type: "run.cancel",
530 runId: run.id,
531 },
532 }).pipe(
533 Effect.andThen(
534 Effect.tryPromise({
535 try: async () => {
536 try {
537 await run.cancel();
538 } catch (cause) {
539 if (!isCursorCancellationError(cause)) {
540 throw cause;
541 }
542 }
543 },
544 catch: (cause) => runnerError(cause, "run.cancel"),
545 }),
546 ),
547 ),
548 } satisfies CursorAgentSdkRun;
549 }),
550 listMessages: log({
551 direction: "outgoing",
552 stage: "decoded",
553 payload: {
554 type: "agent.messages.list",
555 agentId: agent.agentId,
556 },
557 }).pipe(
558 Effect.andThen(
559 Effect.tryPromise({
560 try: () =>
561 Agent.messages.list(agent.agentId, {
562 runtime: "local",
563 ...(cwd === undefined ? {} : { cwd }),
564 }),
565 catch: (cause) => runnerError(cause, "agent.messages.list"),
566 }),
567 ),
568 Effect.tap((messages) =>
569 log({
570 direction: "incoming",
571 stage: "decoded",
572 payload: {
573 type: "agent.messages",
574 agentId: agent.agentId,
575 messages,
576 },
577 }),
578 ),
579 ),
580 close: log({
581 direction: "outgoing",
582 stage: "decoded",
583 payload: {
584 type: "agent.close",
585 agentId: agent.agentId,
586 },
587 }).pipe(
588 Effect.andThen(
589 Effect.try({
590 try: () => agent.close(),
591 catch: (cause) => runnerError(cause, "agent.close"),
592 }),
593 ),
594 ),
595 } satisfies CursorAgentSdkSession;
596 }),
597 assertComplete: Effect.void,
598 });
599});
600
601export const layer = Layer.effect(
602 CursorAgentSdkRunner,
603 Effect.gen(function* () {
604 const { native: nativeEventLogger } = yield* ProviderEventLoggers.ProviderEventLoggers;
605 return yield* makeCursorAgentSdkRunner((input) =>
606 makeCursorAgentSdkProtocolLogger({
607 nativeEventLogger,
608 threadId: input.threadId,
609 providerSessionId: input.providerSessionId,
610 }),
611 );
612 }),
613);