1import type {
2 AgentMessage,
3 AgentOptions,
4 InteractionUpdate,
5 McpServerConfig,
6 RunResult,
7 SDKUserMessage,
8 SettingSource,
9 ToolCall,
10} from "@cursor/sdk";
11import { formatReadToolLabel, formatSearchToolLabel } from "@t3tools/shared/toolActivity";
12import * as HostProcess from "@t3tools/shared/HostProcess";
13import {
14 isOrchestrationV2WorkActive,
15 defaultInstanceIdForDriver,
16 type ChatAttachment,
17 type ModelSelection,
18 type OrchestrationV2ConversationMessage,
19 type OrchestrationV2ExecutionNode,
20 type OrchestrationV2PlanArtifact,
21 type OrchestrationV2PlanStep,
22 type OrchestrationV2ProviderCapabilities,
23 type OrchestrationV2ProviderFailure,
24 type OrchestrationV2ProviderSession,
25 type OrchestrationV2ProviderThread,
26 type OrchestrationV2ProviderTurn,
27 type OrchestrationV2Subagent,
28 type OrchestrationV2TurnItem,
29 type ProviderInstanceId,
30 type ThreadId,
31} from "@t3tools/contracts";
32import { CursorSettings } from "../settings.ts";
33import * as DateTime from "effect/DateTime";
34import * as Deferred from "effect/Deferred";
35import * as Effect from "effect/Effect";
36import * as FileSystem from "effect/FileSystem";
37import * as Option from "effect/Option";
38import * as Path from "effect/Path";
39import * as Queue from "effect/Queue";
40import * as Ref from "effect/Ref";
41import * as Schema from "effect/Schema";
42import * as Stream from "effect/Stream";
43
44import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
45import { mcpToolPresentation } from "@t3tools/provider-core/server/mcpToolPresentation";
46import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
47import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
48import { CursorTransportFailure } from "./transportFailure.ts";
49import { cursorSdkModelSelection } from "./sdkModel.ts";
50import {
51 discoverCursorSkills,
52 hasCursorSkillMention,
53 rewriteCursorSkillMentions,
54} from "./skills.ts";
55import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
56import { t3OrchestrationPromptForFirstRun } from "@t3tools/provider-core/server/orchestrationInstructions";
57import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
58import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
59import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
60import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition";
61import {
62 isProviderNativeImageAttachment,
63 providerMessageTextWithAttachmentPaths,
64} from "@t3tools/provider-core/server/attachmentPrompt";
65import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
66import {
67 ProviderAdapterDriverCreateError,
68 type ProviderAdapterDriver,
69 type ProviderAdapterDriverCreateInput,
70} from "@t3tools/provider-core/server/adapterDriver";
71import {
72 makeSubagentChildThread,
73 makeSubagentConversationArtifacts,
74 subagentThreadTitle,
75} from "@t3tools/provider-core/server/subagentProjection";
76import * as CursorAgentSdk from "./CursorAgentSdk.ts";
77export { cursorSdkModelSelection } from "./sdkModel.ts";
78
79export const CURSOR_DRIVER_KIND = CursorAgentSdk.CURSOR_PROVIDER;
80export const CURSOR_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(CURSOR_DRIVER_KIND);
81const DEFAULT_CURSOR_SETTINGS = Schema.decodeSync(CursorSettings)({});
82
83export const CursorProviderCapabilitiesV2 = {
84 sessions: {
85 supportsMultipleProviderThreadsPerSession: false,
86 supportsModelSwitchInSession: true,
87 supportsProviderSwitchingViaHandoff: true,
88 supportsRuntimeModeSwitchInSession: false,
89 pendingRequestsSurviveRestart: false,
90 },
91 threads: {
92 canCreateEmptyThread: true,
93 canReadThreadSnapshot: true,
94 canRollbackThread: false,
95 canForkThread: false,
96 canForkFromTurn: false,
97 canForkFromSubagentThread: false,
98 exposesNativeThreadId: true,
99 },
100 turns: {
101 exposesNativeTurnId: true,
102 emitsTurnStarted: true,
103 emitsTurnCompleted: true,
104 supportsInterrupt: true,
105 supportsActiveSteering: false,
106 supportsSteeringByInterruptRestart: true,
107 supportsQueuedMessages: true,
108 terminalStatusQuality: "strong",
109 },
110 streaming: {
111 streamsAssistantText: true,
112 streamsReasoning: true,
113 streamsToolOutput: true,
114 streamsPlanText: true,
115 emitsMessageCompleted: true,
116 },
117 tools: {
118 exposesToolItemIds: true,
119 emitsToolStarted: true,
120 emitsToolCompleted: true,
121 emitsToolOutput: true,
122 supportsMcpTools: true,
123 supportsDynamicToolCallbacks: false,
124 },
125 approvals: {
126 supportsCommandApproval: false,
127 supportsFileReadApproval: false,
128 supportsFileChangeApproval: false,
129 supportsApplyPatchApproval: false,
130 approvalsHaveNativeRequestIds: false,
131 approvalCallbacksAreLiveOnly: false,
132 approvalsCanOriginateFromSubagents: false,
133 },
134 planning: {
135 emitsPlanUpdated: true,
136 emitsTodoList: true,
137 emitsProposedPlan: true,
138 supportsStructuredQuestions: false,
139 planDeltasHaveItemIds: true,
140 },
141 subagents: {
142 supportsSubagents: true,
143 exposesSubagentThreadIds: false,
144 emitsSubagentLifecycle: true,
145 canWaitForSubagents: true,
146 canCloseSubagents: false,
147 canForkSubagentThread: false,
148 },
149 context: {
150 acceptsSystemContext: false,
151 acceptsDeveloperContext: false,
152 acceptsSyntheticUserContext: true,
153 canGenerateSummaries: true,
154 canConsumeHandoffSummaries: true,
155 supportsDeltaHandoff: true,
156 supportsFullThreadHandoff: true,
157 maxRecommendedHandoffChars: null,
158 },
159 checkpointing: {
160 appCanCheckpointFilesystem: true,
161 supportsNestedCheckpointScopes: true,
162 providerCanRollbackConversation: false,
163 providerRollbackReturnsSnapshot: false,
164 providerCanReadConversationSnapshot: true,
165 },
166 identity: {
167 nativeThreadIds: "strong",
168 nativeTurnIds: "strong",
169 nativeItemIds: "weak",
170 nativeRequestIds: "none",
171 },
172 runtimePolicy: {
173 enforcement: "native",
174 },
175} satisfies OrchestrationV2ProviderCapabilities;
176
177export interface CursorRuntimeAgentPolicy {
178 readonly autoReview: boolean;
179 readonly sandboxEnabled: boolean;
180}
181
182export function cursorRuntimeAgentPolicy(
183 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy,
184): CursorRuntimeAgentPolicy {
185 const sandboxPolicyType =
186 typeof runtimePolicy.sandboxPolicy === "object" &&
187 runtimePolicy.sandboxPolicy !== null &&
188 "type" in runtimePolicy.sandboxPolicy &&
189 typeof runtimePolicy.sandboxPolicy.type === "string"
190 ? runtimePolicy.sandboxPolicy.type
191 : undefined;
192 return {
193 autoReview:
194 runtimePolicy.approvalPolicy === undefined
195 ? runtimePolicy.runtimeMode === "approval-required"
196 : runtimePolicy.approvalPolicy !== "never",
197 sandboxEnabled:
198 runtimePolicy.sandboxPolicy === undefined
199 ? runtimePolicy.runtimeMode !== "full-access"
200 : sandboxPolicyType !== "dangerFullAccess",
201 };
202}
203
204export function cursorMcpServers(
205 session: McpProviderSession.McpProviderSessionConfig | undefined,
206): Record<string, McpServerConfig> | undefined {
207 if (session === undefined) {
208 return undefined;
209 }
210 return {
211 "t3-code": {
212 type: "http",
213 url: session.endpoint,
214 headers: {
215 Authorization: session.authorizationHeader,
216 },
217 },
218 };
219}
220
221function providerSession(input: {
222 readonly providerSessionId: OrchestrationV2ProviderSession["id"];
223 readonly providerInstanceId: ProviderInstanceId;
224 readonly cwd: string | null;
225 readonly model: string;
226 readonly now: DateTime.Utc;
227}): OrchestrationV2ProviderSession {
228 return {
229 id: input.providerSessionId,
230 driver: CursorAgentSdk.CURSOR_PROVIDER,
231 providerInstanceId: input.providerInstanceId,
232 status: "ready",
233 cwd: input.cwd ?? process.cwd(),
234 model: input.model,
235 capabilities: CursorProviderCapabilitiesV2,
236 createdAt: input.now,
237 updatedAt: input.now,
238 lastError: null,
239 };
240}
241
242function makeProviderThread(input: {
243 readonly idAllocator: IdAllocator.IdAllocatorV2["Service"];
244 readonly providerInstanceId: ProviderInstanceId;
245 readonly appThreadId: OrchestrationV2ProviderThread["appThreadId"];
246 readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"];
247 readonly nativeThreadId: string;
248 readonly ownerNodeId?: OrchestrationV2ProviderThread["ownerNodeId"];
249 readonly forkedFrom?: OrchestrationV2ProviderThread["forkedFrom"];
250 readonly now: DateTime.Utc;
251}): OrchestrationV2ProviderThread {
252 return {
253 id: input.idAllocator.derive.providerThread({
254 driver: CursorAgentSdk.CURSOR_PROVIDER,
255 nativeThreadId: input.nativeThreadId,
256 }),
257 driver: CursorAgentSdk.CURSOR_PROVIDER,
258 providerInstanceId: input.providerInstanceId,
259 providerSessionId: input.providerSessionId,
260 appThreadId: input.appThreadId,
261 ownerNodeId: input.ownerNodeId ?? null,
262 nativeThreadRef: {
263 driver: CursorAgentSdk.CURSOR_PROVIDER,
264 nativeId: input.nativeThreadId,
265 strength: "strong",
266 },
267 nativeConversationHeadRef: null,
268 status: "idle",
269 firstRunOrdinal: null,
270 lastRunOrdinal: null,
271 handoffIds: [],
272 forkedFrom: input.forkedFrom ?? null,
273 createdAt: input.now,
274 updatedAt: input.now,
275 };
276}
277
278function nativeThreadId(providerThread: OrchestrationV2ProviderThread): string {
279 const id = providerThread.nativeThreadRef?.nativeId;
280 if (id === null || id === undefined) {
281 throw new ProviderAdapter.ProviderAdapterProtocolError({
282 driver: CursorAgentSdk.CURSOR_PROVIDER,
283 detail: `Provider thread ${providerThread.id} is missing its Cursor agent id.`,
284 });
285 }
286 return id;
287}
288
289/**
290 * Every Cursor settings layer the Cursor CLI loads: project and user rules,
291 * skills, hooks, and MCP servers, team and MDM admin policy, and account
292 * plugins. The SDK loads none of them when `settingSources` is omitted.
293 * Sandbox policy files are read either way, and hooks can only deny or ask
294 * (which local SDK runs reject), so these layers do not loosen the sandbox or
295 * approval mode T3 sets.
296 */
297const CURSOR_AGENT_SETTING_SOURCES = [
298 "project",
299 "user",
300 "team",
301 "mdm",
302 "plugins",
303] as const satisfies ReadonlyArray<SettingSource>;
304
305export function makeCursorAgentOptions(input: {
306 readonly apiKey?: string;
307 readonly modelSelection: ModelSelection;
308 readonly runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy;
309 readonly threadId: ThreadId;
310 readonly mcpSession: McpProviderSession.McpProviderSessionConfig | undefined;
311}): AgentOptions {
312 const policy = cursorRuntimeAgentPolicy(input.runtimePolicy);
313 const mcpServers = cursorMcpServers(input.mcpSession);
314 return {
315 model: cursorSdkModelSelection(input.modelSelection),
316 name: `T3 Code ${input.threadId}`,
317 mode: input.runtimePolicy.interactionMode === "plan" ? "plan" : "agent",
318 ...(input.apiKey === undefined ? {} : { apiKey: input.apiKey }),
319 local: {
320 ...(input.runtimePolicy.cwd === null ? {} : { cwd: input.runtimePolicy.cwd }),
321 autoReview: policy.autoReview,
322 settingSources: [...CURSOR_AGENT_SETTING_SOURCES],
323 sandboxOptions: {
324 enabled: policy.sandboxEnabled,
325 },
326 enableAgentRetries: true,
327 },
328 ...(mcpServers === undefined ? {} : { mcpServers }),
329 };
330}
331
332function stableJson(value: unknown): string {
333 try {
334 return JSON.stringify(value, null, 2);
335 } catch {
336 return String(value);
337 }
338}
339
340function unknownRecord(value: unknown): Record<string, unknown> | undefined {
341 return typeof value === "object" && value !== null && !Array.isArray(value)
342 ? (value as Record<string, unknown>)
343 : undefined;
344}
345
346function nestedString(value: unknown, keys: ReadonlyArray<string>): string | undefined {
347 let current: unknown = value;
348 for (const key of keys) {
349 current = unknownRecord(current)?.[key];
350 }
351 return typeof current === "string" ? current : undefined;
352}
353
354function cursorToolFailed(toolCall: ToolCall): boolean {
355 if (toolCall.result?.status === "error") {
356 return true;
357 }
358 return toolCall.type === "mcp" && toolCall.result?.status === "success"
359 ? toolCall.result.value.isError
360 : false;
361}
362
363function cursorToolOutput(toolCall: ToolCall): unknown {
364 const result = toolCall.result;
365 if (result === undefined) {
366 return undefined;
367 }
368 return result.status === "success" ? result.value : result.error;
369}
370
371function cursorToolOutputText(toolCall: ToolCall): string {
372 if (toolCall.type === "shell" && toolCall.result?.status === "success") {
373 return [toolCall.result.value.stdout, toolCall.result.value.stderr]
374 .filter((part) => part.length > 0)
375 .join("\n");
376 }
377 if (toolCall.type === "mcp" && toolCall.result?.status === "success") {
378 return toolCall.result.value.content
379 .flatMap((part) => (part.text === undefined ? [] : [part.text.text]))
380 .join("\n");
381 }
382 const output = cursorToolOutput(toolCall);
383 if (output === undefined) {
384 return "";
385 }
386 return typeof output === "string" ? output : stableJson(output);
387}
388
389function cursorToolName(toolCall: ToolCall): string {
390 if (toolCall.type !== "mcp") {
391 return toolCall.type;
392 }
393 const provider = toolCall.args.providerIdentifier ?? "mcp";
394 const tool = toolCall.args.toolName ?? "unknown";
395 return `mcp__${provider}__${tool}`;
396}
397
398function cursorToolFileName(toolCall: ToolCall): string {
399 switch (toolCall.type) {
400 case "write":
401 case "delete":
402 case "read":
403 case "edit":
404 case "ls":
405 return toolCall.args.path;
406 case "generateImage":
407 return toolCall.args.filePath ?? "generated-image";
408 default:
409 return cursorToolName(toolCall);
410 }
411}
412
413function cursorToolSearchPattern(toolCall: ToolCall): string | undefined {
414 switch (toolCall.type) {
415 case "glob":
416 return toolCall.args.globPattern;
417 case "grep":
418 return toolCall.args.pattern;
419 case "semSearch":
420 return toolCall.args.query;
421 case "ls":
422 return toolCall.args.path;
423 case "readLints":
424 return toolCall.args.paths.join(", ");
425 default:
426 return undefined;
427 }
428}
429
430type CursorLsDirectoryNode = Extract<
431 Extract<ToolCall, { readonly type: "ls" }>["result"],
432 { readonly status: "success" }
433>["value"]["directoryTreeRoot"];
434
435function cursorLsSearchResults(
436 node: CursorLsDirectoryNode,
437 path: Path.Path,
438): ReadonlyArray<{ readonly fileName: string }> {
439 return [
440 ...node.childrenFiles.map((file) => ({ fileName: path.join(node.absPath, file.name) })),
441 ...node.childrenDirs.flatMap((child) => cursorLsSearchResults(child, path)),
442 ];
443}
444
445function cursorToolSearchResults(
446 toolCall: ToolCall,
447 path: Path.Path,
448): ReadonlyArray<{
449 readonly fileName: string;
450 readonly line?: number;
451 readonly column?: number;
452 readonly preview?: string;
453}> {
454 if (toolCall.result?.status !== "success") {
455 return [];
456 }
457 switch (toolCall.type) {
458 case "glob":
459 return toolCall.result.value.files.map((fileName) => ({ fileName }));
460 case "grep":
461 return Object.values(toolCall.result.value.workspaceResults ?? {}).flatMap((result) => {
462 if (result.type === "files") {
463 return result.output.files.map((fileName) => ({ fileName }));
464 }
465 if (result.type === "count") {
466 return result.output.counts.map((entry) => ({
467 fileName: entry.file,
468 preview: `${entry.count} matches`,
469 }));
470 }
471 return result.output.matches.map((entry) => ({
472 fileName: entry.file,
473 ...(entry.lineNumber === undefined ? {} : { line: entry.lineNumber }),
474 preview: entry.line,
475 }));
476 });
477 case "ls":
478 return cursorLsSearchResults(toolCall.result.value.directoryTreeRoot, path);
479 case "readLints":
480 return toolCall.result.value.fileDiagnostics.flatMap((file) =>
481 file.diagnostics.map((diagnostic) => {
482 const line = diagnostic.range?.start?.line;
483 const character = diagnostic.range?.start?.character;
484 const hasLine = typeof line === "number" && Number.isInteger(line) && line >= 0;
485 return {
486 fileName: file.path,
487 ...(hasLine ? { line: line + 1 } : {}),
488 ...(hasLine &&
489 typeof character === "number" &&
490 Number.isInteger(character) &&
491 character >= 0
492 ? { column: character + 1 }
493 : {}),
494 preview: diagnostic.message,
495 };
496 }),
497 );
498 case "semSearch":
499 return [
500 {
501 fileName: "semantic-search",
502 preview: toolCall.result.value.results,
503 },
504 ];
505 default:
506 return [];
507 }
508}
509
510function cursorTodoSteps(
511 toolCall: Extract<ToolCall, { readonly type: "updateTodos" }>,
512): ReadonlyArray<OrchestrationV2PlanStep> {
513 const todos =
514 toolCall.result?.status === "success" ? toolCall.result.value.todos : toolCall.args.todos;
515 return todos
516 .filter((todo) => todo.status !== "cancelled" && todo.content.trim().length > 0)
517 .map((todo, index) => ({
518 id: `todo-${index + 1}`,
519 text: todo.content,
520 status:
521 todo.status === "inProgress"
522 ? "running"
523 : todo.status === "completed"
524 ? "completed"
525 : "pending",
526 }));
527}
528
529function assistantTextsFromConversationSteps(steps: ReadonlyArray<unknown>): ReadonlyArray<string> {
530 return steps.flatMap((step) => {
531 const record = unknownRecord(step);
532 if (record?.type === "assistantMessage") {
533 const text = nestedString(record, ["message", "text"]);
534 return text === undefined || text.length === 0 ? [] : [text];
535 }
536 const text = nestedString(record, ["assistantMessage", "text"]);
537 return text === undefined || text.length === 0 ? [] : [text];
538 });
539}
540
541function nestedToolResult<Value = Record<string, unknown>>(
542 rawResult: unknown,
543 mapSuccess: (success: Record<string, unknown>) => Value = (success) => success as Value,
544):
545 | { readonly status: "success"; readonly value: Value }
546 | { readonly status: "error"; readonly error: unknown }
547 | undefined {
548 const result = unknownRecord(rawResult);
549 if (result === undefined) {
550 return undefined;
551 }
552 const success = unknownRecord(result.success);
553 if (success !== undefined) {
554 return {
555 status: "success",
556 value: mapSuccess(success),
557 };
558 }
559 return {
560 status: "error",
561 error: result.error ?? result.permissionDenied ?? result,
562 };
563}
564
565function nestedGrepWorkspaceResults(success: Record<string, unknown>): Record<string, unknown> {
566 const workspaces = unknownRecord(success.workspaceResults);
567 if (workspaces === undefined) {
568 return {};
569 }
570 return Object.fromEntries(
571 Object.entries(workspaces).map(([workspace, rawWorkspaceResult]) => {
572 const workspaceResult = unknownRecord(rawWorkspaceResult);
573 const content = unknownRecord(workspaceResult?.content);
574 const rawMatches = Array.isArray(content?.matches) ? content.matches : [];
575 const matches = rawMatches.flatMap((rawFileMatch) => {
576 const fileMatch = unknownRecord(rawFileMatch);
577 const file = typeof fileMatch?.file === "string" ? fileMatch.file : "";
578 const lineMatches = Array.isArray(fileMatch?.matches) ? fileMatch.matches : [];
579 return lineMatches.flatMap((rawLineMatch) => {
580 const lineMatch = unknownRecord(rawLineMatch);
581 if (lineMatch === undefined) {
582 return [];
583 }
584 const line = typeof lineMatch.content === "string" ? lineMatch.content : undefined;
585 if (file.length === 0 || line === undefined) {
586 return [];
587 }
588 return [
589 {
590 file,
591 line,
592 ...(typeof lineMatch.lineNumber === "number"
593 ? { lineNumber: lineMatch.lineNumber }
594 : {}),
595 },
596 ];
597 });
598 });
599 return [
600 workspace,
601 {
602 type: "content",
603 output: {
604 matches,
605 totalMatches:
606 typeof content?.totalMatchedLines === "number"
607 ? content.totalMatchedLines
608 : matches.length,
609 },
610 },
611 ];
612 }),
613 );
614}
615
616export function nestedToolCallFromEnvelope(
617 envelope: Record<string, unknown>,
618): { readonly callId: string; readonly toolCall: ToolCall } | undefined {
619 const callId = typeof envelope.toolCallId === "string" ? envelope.toolCallId : undefined;
620 const wrapperEntry = Object.entries(envelope).find(([key]) => key.endsWith("ToolCall"));
621 if (wrapperEntry === undefined) {
622 return undefined;
623 }
624 const [wrapperName, rawCall] = wrapperEntry;
625 const call = unknownRecord(rawCall);
626 if (call === undefined) {
627 return undefined;
628 }
629 const args = unknownRecord(call.args) ?? {};
630 const fallbackCallId = `nested-${wrapperName}`;
631 const nestedCallId = callId ?? fallbackCallId;
632
633 switch (wrapperName) {
634 case "readToolCall": {
635 const success = unknownRecord(unknownRecord(call.result)?.success);
636 const path =
637 typeof args.path === "string"
638 ? args.path
639 : typeof success?.path === "string"
640 ? success.path
641 : "<unknown path>";
642 return {
643 callId: nestedCallId,
644 toolCall: {
645 type: "read",
646 args: { path },
647 result: nestedToolResult(call.result, (value) => ({
648 content: typeof value.content === "string" ? value.content : "",
649 totalLines: typeof value.totalLines === "number" ? value.totalLines : 0,
650 fileSize: typeof value.fileSize === "number" ? value.fileSize : 0,
651 })),
652 } as unknown as ToolCall,
653 };
654 }
655 case "globToolCall":
656 return {
657 callId: nestedCallId,
658 toolCall: {
659 type: "glob",
660 args: {
661 globPattern: typeof args.globPattern === "string" ? args.globPattern : "**/*",
662 ...(typeof args.targetDirectory === "string"
663 ? { targetDirectory: args.targetDirectory }
664 : {}),
665 },
666 result: nestedToolResult(call.result, (value) => ({
667 files: Array.isArray(value.files)
668 ? value.files.filter((file): file is string => typeof file === "string")
669 : [],
670 totalFiles: typeof value.totalFiles === "number" ? value.totalFiles : 0,
671 clientTruncated: false,
672 ripgrepTruncated: false,
673 })),
674 } as unknown as ToolCall,
675 };
676 case "grepToolCall":
677 return {
678 callId: nestedCallId,
679 toolCall: {
680 type: "grep",
681 args: {
682 pattern: typeof args.pattern === "string" ? args.pattern : "",
683 ...(typeof args.path === "string" ? { path: args.path } : {}),
684 ...(typeof args.glob === "string" ? { glob: args.glob } : {}),
685 ...(typeof args.outputMode === "string" ? { outputMode: args.outputMode } : {}),
686 ...(typeof args.caseInsensitive === "boolean"
687 ? { caseInsensitive: args.caseInsensitive }
688 : {}),
689 ...(typeof args.offset === "number" ? { offset: args.offset } : {}),
690 ...(typeof args.multiline === "boolean" ? { multiline: args.multiline } : {}),
691 },
692 result: nestedToolResult(call.result, (value) => ({
693 workspaceResults: nestedGrepWorkspaceResults(value),
694 })),
695 } as unknown as ToolCall,
696 };
697 case "shellToolCall": {
698 const rawResult = unknownRecord(call.result);
699 const permissionDenied = unknownRecord(rawResult?.permissionDenied);
700 const command =
701 typeof args.command === "string"
702 ? args.command
703 : typeof permissionDenied?.command === "string"
704 ? permissionDenied.command
705 : "<unknown command>";
706 return {
707 callId: nestedCallId,
708 toolCall: {
709 type: "shell",
710 args: { command },
711 result: nestedToolResult(call.result, (value) => ({
712 stdout: typeof value.stdout === "string" ? value.stdout : "",
713 stderr: typeof value.stderr === "string" ? value.stderr : "",
714 exitCode: typeof value.exitCode === "number" ? value.exitCode : 0,
715 signal: typeof value.signal === "string" ? value.signal : "",
716 executionTime: typeof value.executionTime === "number" ? value.executionTime : 0,
717 })),
718 } as unknown as ToolCall,
719 };
720 }
721 default: {
722 const type = wrapperName.slice(0, -"ToolCall".length);
723 return {
724 callId: nestedCallId,
725 toolCall: {
726 type,
727 args,
728 result: nestedToolResult(call.result),
729 } as unknown as ToolCall,
730 };
731 }
732 }
733}
734
735function toolCallsFromConversationSteps(
736 steps: ReadonlyArray<unknown>,
737): ReadonlyArray<{ readonly callId: string; readonly toolCall: ToolCall }> {
738 return steps.flatMap((step) => {
739 const record = unknownRecord(step);
740 if (record?.type === "toolCall") {
741 const message = unknownRecord(record.message);
742 return typeof message?.type === "string"
743 ? [
744 {
745 callId: typeof record.callId === "string" ? record.callId : `nested-${message.type}`,
746 toolCall: message as ToolCall,
747 },
748 ]
749 : [];
750 }
751 const envelope = unknownRecord(record?.toolCall);
752 const nested = envelope === undefined ? undefined : nestedToolCallFromEnvelope(envelope);
753 return nested === undefined ? [] : [nested];
754 });
755}
756
757function textFromAgentMessage(message: AgentMessage): string {
758 const value = message.message;
759 if (typeof value === "string") {
760 return value;
761 }
762 const direct =
763 nestedString(value, ["text"]) ??
764 nestedString(value, ["message", "text"]) ??
765 nestedString(value, ["userMessage", "text"]) ??
766 nestedString(value, ["assistantMessage", "text"]);
767 if (direct !== undefined) {
768 return direct;
769 }
770 const content = unknownRecord(value)?.content;
771 if (!Array.isArray(content)) {
772 return "";
773 }
774 return content
775 .flatMap((part) => {
776 const text = unknownRecord(part)?.text;
777 return typeof text === "string" ? [text] : [];
778 })
779 .join("");
780}
781
782interface CursorProjectionTarget {
783 readonly threadId: ThreadId;
784 readonly runId: ProviderAdapter.ProviderAdapterV2TurnInput["runId"] | null;
785 readonly rootNodeId: OrchestrationV2ExecutionNode["rootNodeId"];
786 readonly parentNodeId: OrchestrationV2ExecutionNode["id"];
787 readonly providerThreadId: OrchestrationV2ProviderThread["id"] | null;
788 readonly providerTurnId: OrchestrationV2ProviderTurn["id"] | null;
789}
790
791interface ActiveCursorToolCall {
792 readonly callId: string;
793 toolCall: ToolCall;
794 readonly target: CursorProjectionTarget;
795 readonly ordinal: number;
796 readonly startedAt: DateTime.Utc;
797 streamedOutput: string;
798}
799
800interface ActiveCursorSubagent {
801 task: OrchestrationV2Subagent;
802 toolCall: Extract<ToolCall, { readonly type: "task" }>;
803 readonly callId: string;
804 readonly childThreadId: ThreadId;
805 readonly childRootNodeId: OrchestrationV2ExecutionNode["id"];
806 readonly turnItemId: OrchestrationV2TurnItem["id"];
807 readonly turnItemOrdinal: number;
808 nextChildOrdinal: number;
809 resultProjected: boolean;
810}
811
812interface ActiveCursorTextSegment {
813 readonly nativeItemId: string;
814 readonly startedAt: DateTime.Utc;
815 text: string;
816}
817
818interface ActiveCursorTextStream {
819 current: ActiveCursorTextSegment | null;
820 nextSegment: number;
821}
822
823interface ActiveCursorTurn {
824 readonly input: ProviderAdapter.ProviderAdapterV2TurnInput;
825 readonly run: CursorAgentSdk.CursorAgentSdkRun;
826 readonly providerTurnId: OrchestrationV2ProviderTurn["id"];
827 readonly startedAt: DateTime.Utc;
828 readonly completed: Deferred.Deferred<void, never>;
829 // Item ordinals allocated in this turn. No later turn looks items up here.
830 readonly itemOrdinals: Map<string, number>;
831 readonly tools: Map<string, ActiveCursorToolCall>;
832 readonly subagents: Map<string, ActiveCursorSubagent>;
833 readonly assistant: ActiveCursorTextStream;
834 readonly assistantReply: CursorTransportFailure;
835 readonly reasoning: ActiveCursorTextStream;
836 interrupted: boolean;
837 finalized: boolean;
838}
839
840interface CursorLiveAgent {
841 readonly nativeThreadId: string;
842 readonly session: CursorAgentSdk.CursorAgentSdkSession;
843}
844
845export interface CursorAdapterV2Options {
846 readonly instanceId: ProviderInstanceId;
847 readonly settings: CursorSettings;
848 readonly environment: NodeJS.ProcessEnv;
849}
850
851export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* (
852 options: CursorAdapterV2Options,
853) {
854 const fileSystem = yield* FileSystem.FileSystem;
855 const path = yield* Path.Path;
856 const idAllocator = yield* IdAllocator.IdAllocatorV2;
857 const runner = yield* CursorAgentSdk.CursorAgentSdkRunner;
858 const host = yield* ProviderHost.ProviderHost;
859 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
860 const adapterOptions = options;
861 const apiKey = adapterOptions.environment.CURSOR_API_KEY?.trim() || undefined;
862
863 return ProviderAdapter.ProviderAdapterV2.of({
864 instanceId: adapterOptions.instanceId,
865 driver: CursorAgentSdk.CURSOR_PROVIDER,
866 getCapabilities: () => Effect.succeed(CursorProviderCapabilitiesV2),
867 planSelectionTransition: () => Effect.succeed(turnScopedSelectionTransition()),
868 openSession: Effect.fn("CursorAdapterV2.openSession")(
869 function* (input: ProviderAdapter.ProviderAdapterV2OpenSessionInput) {
870 const sessionScope = yield* Effect.scope;
871 const createdAt = yield* DateTime.now;
872 const session = providerSession({
873 providerSessionId: input.providerSessionId,
874 providerInstanceId: adapterOptions.instanceId,
875 cwd: input.runtimePolicy.cwd,
876 model: input.modelSelection.model,
877 now: createdAt,
878 });
879 const events = yield* Queue.unbounded<ProviderAdapter.ProviderAdapterV2Event>();
880 const liveAgent = yield* Ref.make<CursorLiveAgent | null>(null);
881 const activeTurn = yield* Ref.make<ActiveCursorTurn | null>(null);
882 const planIds = yield* Ref.make(new Map<string, OrchestrationV2PlanArtifact["id"]>());
883
884 const emitProviderEvent = (event: ProviderAdapter.ProviderAdapterV2Event) =>
885 Queue.offer(events, event).pipe(Effect.asVoid);
886
887 const resolveItemOrdinal = (context: ActiveCursorTurn, nativeItemId: string) =>
888 Effect.sync(() => {
889 const existing = context.itemOrdinals.get(nativeItemId);
890 if (existing !== undefined) {
891 return existing;
892 }
893 const ordinal = context.input.providerTurnOrdinal * 100 + context.itemOrdinals.size + 1;
894 context.itemOrdinals.set(nativeItemId, ordinal);
895 return ordinal;
896 });
897
898 const resolvePlanId = Effect.fnUntraced(function* (
899 context: ActiveCursorTurn,
900 nativeItemId: string,
901 ) {
902 const existing = (yield* Ref.get(planIds)).get(nativeItemId);
903 if (existing !== undefined) {
904 return existing;
905 }
906 const planId = yield* idAllocator.allocate.plan({
907 threadId: context.input.threadId,
908 runId: context.input.runId,
909 driver: CursorAgentSdk.CURSOR_PROVIDER,
910 });
911 yield* Ref.update(planIds, (current) => {
912 const updated = new Map(current);
913 updated.set(nativeItemId, planId);
914 return updated;
915 });
916 return planId;
917 });
918
919 const parentTarget = (context: ActiveCursorTurn): CursorProjectionTarget => ({
920 threadId: context.input.threadId,
921 runId: context.input.runId,
922 rootNodeId: context.input.rootNodeId,
923 parentNodeId: context.input.rootNodeId,
924 providerThreadId: context.input.providerThread.id,
925 providerTurnId: context.providerTurnId,
926 });
927
928 const emitAssistant = Effect.fnUntraced(function* (
929 context: ActiveCursorTurn,
930 completed: boolean,
931 ) {
932 const segment = context.assistant.current;
933 if (segment === null || segment.text.length === 0) {
934 return;
935 }
936 const now = yield* DateTime.now;
937 const ordinal = yield* resolveItemOrdinal(context, segment.nativeItemId);
938 const nodeId = idAllocator.derive.nodeFromProviderItem({
939 driver: CursorAgentSdk.CURSOR_PROVIDER,
940 nativeItemId: segment.nativeItemId,
941 });
942 const messageId = idAllocator.derive.messageFromProviderItem({
943 driver: CursorAgentSdk.CURSOR_PROVIDER,
944 nativeItemId: segment.nativeItemId,
945 });
946 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
947 driver: CursorAgentSdk.CURSOR_PROVIDER,
948 nativeItemId: segment.nativeItemId,
949 });
950 const nativeItemRef = {
951 driver: CursorAgentSdk.CURSOR_PROVIDER,
952 nativeId: segment.nativeItemId,
953 strength: "weak" as const,
954 };
955 yield* emitProviderEvent({
956 type: "node.updated",
957 driver: CursorAgentSdk.CURSOR_PROVIDER,
958 node: {
959 id: nodeId,
960 threadId: context.input.threadId,
961 runId: context.input.runId,
962 parentNodeId: context.input.rootNodeId,
963 rootNodeId: context.input.rootNodeId,
964 kind: "assistant_message",
965 status: completed ? "completed" : "running",
966 countsForRun: false,
967 providerThreadId: context.input.providerThread.id,
968 providerTurnId: context.providerTurnId,
969 nativeItemRef,
970 runtimeRequestId: null,
971 checkpointScopeId: null,
972 startedAt: segment.startedAt,
973 completedAt: completed ? now : null,
974 },
975 });
976 yield* emitProviderEvent({
977 type: "message.updated",
978 driver: CursorAgentSdk.CURSOR_PROVIDER,
979 message: {
980 createdBy: "agent",
981 creationSource: "provider",
982 id: messageId,
983 threadId: context.input.threadId,
984 runId: context.input.runId,
985 nodeId,
986 role: "assistant",
987 text: segment.text,
988 attachments: [],
989 streaming: !completed,
990 createdAt: segment.startedAt,
991 updatedAt: now,
992 },
993 });
994 yield* emitProviderEvent({
995 type: "turn_item.updated",
996 driver: CursorAgentSdk.CURSOR_PROVIDER,
997 turnItem: {
998 id: turnItemId,
999 threadId: context.input.threadId,
1000 runId: context.input.runId,
1001 nodeId,
1002 providerThreadId: context.input.providerThread.id,
1003 providerTurnId: context.providerTurnId,
1004 nativeItemRef,
1005 parentItemId: null,
1006 ordinal,
1007 status: completed ? "completed" : "running",
1008 title: null,
1009 startedAt: segment.startedAt,
1010 completedAt: completed ? now : null,
1011 updatedAt: now,
1012 type: "assistant_message",
1013 messageId,
1014 text: segment.text,
1015 streaming: !completed,
1016 },
1017 });
1018 });
1019
1020 const emitReasoning = Effect.fnUntraced(function* (
1021 context: ActiveCursorTurn,
1022 completed: boolean,
1023 ) {
1024 const segment = context.reasoning.current;
1025 if (segment === null || segment.text.length === 0) {
1026 return;
1027 }
1028 const now = yield* DateTime.now;
1029 const ordinal = yield* resolveItemOrdinal(context, segment.nativeItemId);
1030 const nodeId = idAllocator.derive.nodeFromProviderItem({
1031 driver: CursorAgentSdk.CURSOR_PROVIDER,
1032 nativeItemId: segment.nativeItemId,
1033 });
1034 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
1035 driver: CursorAgentSdk.CURSOR_PROVIDER,
1036 nativeItemId: segment.nativeItemId,
1037 });
1038 const nativeItemRef = {
1039 driver: CursorAgentSdk.CURSOR_PROVIDER,
1040 nativeId: segment.nativeItemId,
1041 strength: "weak" as const,
1042 };
1043 yield* emitProviderEvent({
1044 type: "node.updated",
1045 driver: CursorAgentSdk.CURSOR_PROVIDER,
1046 node: {
1047 id: nodeId,
1048 threadId: context.input.threadId,
1049 runId: context.input.runId,
1050 parentNodeId: context.input.rootNodeId,
1051 rootNodeId: context.input.rootNodeId,
1052 kind: "reasoning",
1053 status: completed ? "completed" : "running",
1054 countsForRun: false,
1055 providerThreadId: context.input.providerThread.id,
1056 providerTurnId: context.providerTurnId,
1057 nativeItemRef,
1058 runtimeRequestId: null,
1059 checkpointScopeId: null,
1060 startedAt: segment.startedAt,
1061 completedAt: completed ? now : null,
1062 },
1063 });
1064 yield* emitProviderEvent({
1065 type: "turn_item.updated",
1066 driver: CursorAgentSdk.CURSOR_PROVIDER,
1067 turnItem: {
1068 id: turnItemId,
1069 threadId: context.input.threadId,
1070 runId: context.input.runId,
1071 nodeId,
1072 providerThreadId: context.input.providerThread.id,
1073 providerTurnId: context.providerTurnId,
1074 nativeItemRef,
1075 parentItemId: null,
1076 ordinal,
1077 status: completed ? "completed" : "running",
1078 title: null,
1079 startedAt: segment.startedAt,
1080 completedAt: completed ? now : null,
1081 updatedAt: now,
1082 type: "reasoning",
1083 text: segment.text,
1084 streaming: !completed,
1085 },
1086 });
1087 });
1088
1089 const appendTextSegment = Effect.fnUntraced(function* (input: {
1090 readonly context: ActiveCursorTurn;
1091 readonly stream: ActiveCursorTextStream;
1092 readonly kind: "assistant" | "reasoning";
1093 readonly text: string;
1094 }) {
1095 if (input.stream.current === null) {
1096 input.stream.nextSegment += 1;
1097 input.stream.current = {
1098 nativeItemId: `${input.kind}:${input.context.run.runId}:${input.stream.nextSegment}`,
1099 startedAt: yield* DateTime.now,
1100 text: "",
1101 };
1102 }
1103 input.stream.current.text += input.text;
1104 });
1105
1106 const completeAssistant = Effect.fnUntraced(function* (context: ActiveCursorTurn) {
1107 if (context.assistant.current === null) {
1108 return;
1109 }
1110 yield* emitAssistant(context, true);
1111 context.assistant.current = null;
1112 });
1113
1114 const completeReasoning = Effect.fnUntraced(function* (context: ActiveCursorTurn) {
1115 if (context.reasoning.current === null) {
1116 return;
1117 }
1118 yield* emitReasoning(context, true);
1119 context.reasoning.current = null;
1120 });
1121
1122 const emitToolArtifacts = Effect.fnUntraced(function* (input: {
1123 readonly active: ActiveCursorToolCall;
1124 readonly completed: boolean;
1125 /** Terminal status for a tool the turn ended before Cursor completed it. */
1126 readonly unfinishedStatus?: "interrupted" | "failed" | "cancelled";
1127 }) {
1128 const { active } = input;
1129 const toolCall = active.toolCall;
1130 const now = yield* DateTime.now;
1131 const failed = input.completed && cursorToolFailed(toolCall);
1132 const status = !input.completed
1133 ? "running"
1134 : (input.unfinishedStatus ?? (failed ? "failed" : "completed"));
1135 const nodeId = idAllocator.derive.nodeFromProviderItem({
1136 driver: CursorAgentSdk.CURSOR_PROVIDER,
1137 nativeItemId: active.callId,
1138 });
1139 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
1140 driver: CursorAgentSdk.CURSOR_PROVIDER,
1141 nativeItemId: active.callId,
1142 });
1143 const nativeItemRef = {
1144 driver: CursorAgentSdk.CURSOR_PROVIDER,
1145 nativeId: active.callId,
1146 strength: "strong" as const,
1147 };
1148 const node: OrchestrationV2ExecutionNode = {
1149 id: nodeId,
1150 threadId: active.target.threadId,
1151 runId: active.target.runId,
1152 parentNodeId: active.target.parentNodeId,
1153 rootNodeId: active.target.rootNodeId,
1154 kind: "tool_call",
1155 status,
1156 countsForRun: false,
1157 providerThreadId: active.target.providerThreadId,
1158 providerTurnId: active.target.providerTurnId,
1159 nativeItemRef,
1160 runtimeRequestId: null,
1161 checkpointScopeId: null,
1162 startedAt: active.startedAt,
1163 completedAt: input.completed ? now : null,
1164 };
1165 const base = {
1166 id: turnItemId,
1167 threadId: active.target.threadId,
1168 runId: active.target.runId,
1169 nodeId,
1170 providerThreadId: active.target.providerThreadId,
1171 providerTurnId: active.target.providerTurnId,
1172 nativeItemRef,
1173 parentItemId: null,
1174 ordinal: active.ordinal,
1175 status,
1176 title: null,
1177 startedAt: active.startedAt,
1178 completedAt: input.completed ? now : null,
1179 updatedAt: now,
1180 } satisfies Pick<
1181 OrchestrationV2TurnItem,
1182 | "id"
1183 | "threadId"
1184 | "runId"
1185 | "nodeId"
1186 | "providerThreadId"
1187 | "providerTurnId"
1188 | "nativeItemRef"
1189 | "parentItemId"
1190 | "ordinal"
1191 | "status"
1192 | "title"
1193 | "startedAt"
1194 | "completedAt"
1195 | "updatedAt"
1196 >;
1197 const outputText = cursorToolOutputText(toolCall) || active.streamedOutput;
1198 let turnItem: OrchestrationV2TurnItem;
1199 switch (toolCall.type) {
1200 case "shell":
1201 turnItem = {
1202 ...base,
1203 type: "command_execution",
1204 input: toolCall.args.command,
1205 ...(outputText.length === 0 ? {} : { output: outputText }),
1206 ...(toolCall.result?.status === "success"
1207 ? { exitCode: toolCall.result.value.exitCode }
1208 : {}),
1209 };
1210 break;
1211 case "write":
1212 case "delete":
1213 case "edit":
1214 case "generateImage":
1215 turnItem = {
1216 ...base,
1217 type: "file_change",
1218 fileName: cursorToolFileName(toolCall),
1219 ...(toolCall.type === "edit" &&
1220 toolCall.result?.status === "success" &&
1221 toolCall.result.value.linesAdded !== undefined
1222 ? { additions: toolCall.result.value.linesAdded }
1223 : {}),
1224 ...(toolCall.type === "edit" &&
1225 toolCall.result?.status === "success" &&
1226 toolCall.result.value.linesRemoved !== undefined
1227 ? { deletions: toolCall.result.value.linesRemoved }
1228 : {}),
1229 ...(toolCall.type === "edit" &&
1230 toolCall.result?.status === "success" &&
1231 toolCall.result.value.diffString !== undefined
1232 ? { diffStr: toolCall.result.value.diffString }
1233 : // A failed change keeps its error where the diff would be.
1234 toolCall.result?.status === "error" && outputText.trim().length > 0
1235 ? { diffStr: outputText }
1236 : {}),
1237 ...(toolCall.type === "write" ? { newStr: toolCall.args.fileText } : {}),
1238 };
1239 break;
1240 case "read":
1241 turnItem = {
1242 ...base,
1243 title: formatReadToolLabel(toolCall.args.path),
1244 type: "dynamic_tool",
1245 toolName: "Read",
1246 input: toolCall.args,
1247 ...(cursorToolOutput(toolCall) === undefined
1248 ? {}
1249 : { output: cursorToolOutput(toolCall) }),
1250 };
1251 break;
1252 case "glob":
1253 case "grep":
1254 case "ls":
1255 case "readLints":
1256 case "semSearch": {
1257 const pattern = cursorToolSearchPattern(toolCall);
1258 const searchPath =
1259 toolCall.type === "grep"
1260 ? toolCall.args.path
1261 : toolCall.type === "glob"
1262 ? toolCall.args.targetDirectory
1263 : toolCall.type === "semSearch"
1264 ? toolCall.args.targetDirectories?.join(", ")
1265 : pattern;
1266 // A failed search keeps its error as one row under the searched path.
1267 const results =
1268 toolCall.result?.status === "error" && outputText.trim().length > 0
1269 ? [{ fileName: searchPath?.trim() || ".", preview: outputText }]
1270 : cursorToolSearchResults(toolCall, path);
1271 turnItem = {
1272 ...base,
1273 title:
1274 formatSearchToolLabel({
1275 input: toolCall.args,
1276 ...(pattern === undefined ? {} : { pattern }),
1277 }) ?? null,
1278 type: "file_search",
1279 ...(pattern === undefined ? {} : { pattern }),
1280 ...(results.length === 0 ? {} : { results: [...results] }),
1281 };
1282 break;
1283 }
1284 default:
1285 turnItem = {
1286 ...base,
1287 type: "dynamic_tool",
1288 ...(toolCall.type === "mcp"
1289 ? mcpToolPresentation({
1290 serverName: toolCall.args.providerIdentifier,
1291 toolName: toolCall.args.toolName,
1292 })
1293 : {}),
1294 toolName: cursorToolName(toolCall),
1295 input: toolCall.args,
1296 ...(cursorToolOutput(toolCall) === undefined
1297 ? {}
1298 : { output: cursorToolOutput(toolCall) }),
1299 };
1300 }
1301 yield* emitProviderEvent({
1302 type: "node.updated",
1303 driver: CursorAgentSdk.CURSOR_PROVIDER,
1304 node,
1305 });
1306 yield* emitProviderEvent({
1307 type: "turn_item.updated",
1308 driver: CursorAgentSdk.CURSOR_PROVIDER,
1309 turnItem,
1310 });
1311 });
1312
1313 const ensureToolStarted = Effect.fnUntraced(function* (
1314 context: ActiveCursorTurn,
1315 callId: string,
1316 toolCall: ToolCall,
1317 ) {
1318 const existing = context.tools.get(callId);
1319 if (existing !== undefined) {
1320 existing.toolCall = toolCall;
1321 return existing;
1322 }
1323 const startedAt = yield* DateTime.now;
1324 const active: ActiveCursorToolCall = {
1325 callId,
1326 toolCall,
1327 target: parentTarget(context),
1328 ordinal: yield* resolveItemOrdinal(context, callId),
1329 startedAt,
1330 streamedOutput: "",
1331 };
1332 context.tools.set(callId, active);
1333 yield* emitToolArtifacts({ active, completed: false });
1334 return active;
1335 });
1336
1337 const emitPlanArtifacts = Effect.fnUntraced(function* (input: {
1338 readonly context: ActiveCursorTurn;
1339 readonly callId: string;
1340 readonly markdown: string;
1341 readonly completed: boolean;
1342 readonly failed: boolean;
1343 }) {
1344 const nativeItemId = `plan:${input.callId}`;
1345 const now = yield* DateTime.now;
1346 const planId = yield* resolvePlanId(input.context, nativeItemId);
1347 const ordinal = yield* resolveItemOrdinal(input.context, nativeItemId);
1348 const nodeId = idAllocator.derive.nodeFromProviderItem({
1349 driver: CursorAgentSdk.CURSOR_PROVIDER,
1350 nativeItemId,
1351 });
1352 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
1353 driver: CursorAgentSdk.CURSOR_PROVIDER,
1354 nativeItemId,
1355 });
1356 const status = input.failed ? "failed" : input.completed ? "completed" : "running";
1357 const nativeItemRef = {
1358 driver: CursorAgentSdk.CURSOR_PROVIDER,
1359 nativeId: input.callId,
1360 strength: "strong" as const,
1361 };
1362 const plan: OrchestrationV2PlanArtifact = {
1363 id: planId,
1364 threadId: input.context.input.threadId,
1365 runId: input.context.input.runId,
1366 nodeId,
1367 kind: "proposed_plan",
1368 status: input.completed ? "active" : "draft",
1369 markdown: input.markdown,
1370 };
1371 yield* emitProviderEvent({
1372 type: "node.updated",
1373 driver: CursorAgentSdk.CURSOR_PROVIDER,
1374 node: {
1375 id: nodeId,
1376 threadId: input.context.input.threadId,
1377 runId: input.context.input.runId,
1378 parentNodeId: input.context.input.rootNodeId,
1379 rootNodeId: input.context.input.rootNodeId,
1380 kind: "plan",
1381 status,
1382 countsForRun: false,
1383 providerThreadId: input.context.input.providerThread.id,
1384 providerTurnId: input.context.providerTurnId,
1385 nativeItemRef,
1386 runtimeRequestId: null,
1387 checkpointScopeId: null,
1388 startedAt: input.context.startedAt,
1389 completedAt: input.completed ? now : null,
1390 },
1391 });
1392 yield* emitProviderEvent({
1393 type: "plan.updated",
1394 driver: CursorAgentSdk.CURSOR_PROVIDER,
1395 plan,
1396 });
1397 yield* emitProviderEvent({
1398 type: "turn_item.updated",
1399 driver: CursorAgentSdk.CURSOR_PROVIDER,
1400 turnItem: {
1401 id: turnItemId,
1402 threadId: input.context.input.threadId,
1403 runId: input.context.input.runId,
1404 nodeId,
1405 providerThreadId: input.context.input.providerThread.id,
1406 providerTurnId: input.context.providerTurnId,
1407 nativeItemRef,
1408 parentItemId: null,
1409 ordinal,
1410 status,
1411 title: null,
1412 startedAt: input.context.startedAt,
1413 completedAt: input.completed ? now : null,
1414 updatedAt: now,
1415 type: "proposed_plan",
1416 planId,
1417 markdown: input.markdown,
1418 streaming: !input.completed,
1419 },
1420 });
1421 });
1422
1423 const emitTodoArtifacts = Effect.fnUntraced(function* (input: {
1424 readonly context: ActiveCursorTurn;
1425 readonly callId: string;
1426 readonly toolCall: Extract<ToolCall, { readonly type: "updateTodos" }>;
1427 readonly completed: boolean;
1428 readonly failed: boolean;
1429 }) {
1430 const nativeItemId = `todos:${input.callId}`;
1431 const now = yield* DateTime.now;
1432 const planId = yield* resolvePlanId(input.context, nativeItemId);
1433 const ordinal = yield* resolveItemOrdinal(input.context, nativeItemId);
1434 const nodeId = idAllocator.derive.nodeFromProviderItem({
1435 driver: CursorAgentSdk.CURSOR_PROVIDER,
1436 nativeItemId,
1437 });
1438 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
1439 driver: CursorAgentSdk.CURSOR_PROVIDER,
1440 nativeItemId,
1441 });
1442 const status = input.failed ? "failed" : input.completed ? "completed" : "running";
1443 const steps = cursorTodoSteps(input.toolCall);
1444 const nativeItemRef = {
1445 driver: CursorAgentSdk.CURSOR_PROVIDER,
1446 nativeId: input.callId,
1447 strength: "strong" as const,
1448 };
1449 yield* emitProviderEvent({
1450 type: "node.updated",
1451 driver: CursorAgentSdk.CURSOR_PROVIDER,
1452 node: {
1453 id: nodeId,
1454 threadId: input.context.input.threadId,
1455 runId: input.context.input.runId,
1456 parentNodeId: input.context.input.rootNodeId,
1457 rootNodeId: input.context.input.rootNodeId,
1458 kind: "todo_list",
1459 status,
1460 countsForRun: false,
1461 providerThreadId: input.context.input.providerThread.id,
1462 providerTurnId: input.context.providerTurnId,
1463 nativeItemRef,
1464 runtimeRequestId: null,
1465 checkpointScopeId: null,
1466 startedAt: input.context.startedAt,
1467 completedAt: input.completed ? now : null,
1468 },
1469 });
1470 yield* emitProviderEvent({
1471 type: "plan.updated",
1472 driver: CursorAgentSdk.CURSOR_PROVIDER,
1473 plan: {
1474 id: planId,
1475 threadId: input.context.input.threadId,
1476 runId: input.context.input.runId,
1477 nodeId,
1478 kind: "todo_list",
1479 status: input.completed ? "active" : "draft",
1480 steps: [...steps],
1481 },
1482 });
1483 yield* emitProviderEvent({
1484 type: "turn_item.updated",
1485 driver: CursorAgentSdk.CURSOR_PROVIDER,
1486 turnItem: {
1487 id: turnItemId,
1488 threadId: input.context.input.threadId,
1489 runId: input.context.input.runId,
1490 nodeId,
1491 providerThreadId: input.context.input.providerThread.id,
1492 providerTurnId: input.context.providerTurnId,
1493 nativeItemRef,
1494 parentItemId: null,
1495 ordinal,
1496 status,
1497 title: null,
1498 startedAt: input.context.startedAt,
1499 completedAt: input.completed ? now : null,
1500 updatedAt: now,
1501 type: "todo_list",
1502 planId,
1503 steps: [...steps],
1504 },
1505 });
1506 });
1507
1508 const emitSubagent = Effect.fnUntraced(function* (input: {
1509 readonly context: ActiveCursorTurn;
1510 readonly callId: string;
1511 readonly toolCall: Extract<ToolCall, { readonly type: "task" }>;
1512 readonly completed: boolean;
1513 readonly status?: OrchestrationV2Subagent["status"];
1514 }) {
1515 const args = input.toolCall.args;
1516 const result =
1517 input.toolCall.result?.status === "success" ? input.toolCall.result.value : undefined;
1518 const existing = input.context.subagents.get(input.callId);
1519 if (
1520 existing !== undefined &&
1521 !isOrchestrationV2WorkActive(existing.task.status) &&
1522 !input.completed
1523 )
1524 return;
1525 const now = yield* DateTime.now;
1526 const status: OrchestrationV2Subagent["status"] =
1527 input.status ??
1528 (input.completed
1529 ? cursorToolFailed(input.toolCall)
1530 ? "failed"
1531 : "completed"
1532 : "running");
1533 const resultText = [
1534 ...assistantTextsFromConversationSteps(result?.conversationSteps ?? []),
1535 ...(result?.resultSuffix === undefined ? [] : [result.resultSuffix]),
1536 ]
1537 .filter((part) => part.trim().length > 0)
1538 .join("\n");
1539 const nativeItemId = input.callId;
1540 const nodeId =
1541 existing?.task.id ??
1542 idAllocator.derive.nodeFromProviderItem({
1543 driver: CursorAgentSdk.CURSOR_PROVIDER,
1544 nativeItemId,
1545 });
1546 const childRootNodeId =
1547 existing?.childRootNodeId ??
1548 idAllocator.derive.nodeFromProviderItem({
1549 driver: CursorAgentSdk.CURSOR_PROVIDER,
1550 nativeItemId: `${nativeItemId}:child-root`,
1551 });
1552 const childThreadId =
1553 existing?.childThreadId ??
1554 idAllocator.derive.threadFromProviderThread({
1555 driver: CursorAgentSdk.CURSOR_PROVIDER,
1556 nativeThreadId: `${input.context.run.runId}:task:${input.callId}`,
1557 });
1558 const task: OrchestrationV2Subagent = {
1559 ...(existing?.task ?? {
1560 id: nodeId,
1561 threadId: input.context.input.threadId,
1562 runId: input.context.input.runId,
1563 parentNodeId: input.context.input.rootNodeId,
1564 origin: "provider_native" as const,
1565 createdBy: "agent" as const,
1566 driver: CursorAgentSdk.CURSOR_PROVIDER,
1567 providerInstanceId: input.context.input.modelSelection.instanceId,
1568 providerThreadId: null,
1569 childThreadId,
1570 nativeTaskRef: {
1571 driver: CursorAgentSdk.CURSOR_PROVIDER,
1572 nativeId: input.callId,
1573 strength: "strong" as const,
1574 },
1575 prompt: args.prompt,
1576 title: args.description,
1577 result: null,
1578 startedAt: now,
1579 }),
1580 model: args.model?.trim() || existing?.task.model || null,
1581 nativeTaskRef: {
1582 driver: CursorAgentSdk.CURSOR_PROVIDER,
1583 nativeId: input.callId,
1584 strength: "strong" as const,
1585 },
1586 status,
1587 result: resultText.length === 0 ? (existing?.task.result ?? null) : resultText,
1588 completedAt: input.completed ? (existing?.task.completedAt ?? now) : null,
1589 updatedAt: now,
1590 };
1591 const subagent: ActiveCursorSubagent = {
1592 task,
1593 toolCall: input.toolCall,
1594 callId: input.callId,
1595 childThreadId,
1596 childRootNodeId,
1597 turnItemId:
1598 existing?.turnItemId ??
1599 idAllocator.derive.turnItemFromProviderItem({
1600 driver: CursorAgentSdk.CURSOR_PROVIDER,
1601 nativeItemId,
1602 }),
1603 turnItemOrdinal:
1604 existing?.turnItemOrdinal ?? (yield* resolveItemOrdinal(input.context, nativeItemId)),
1605 nextChildOrdinal: existing?.nextChildOrdinal ?? 100,
1606 resultProjected: existing?.resultProjected ?? false,
1607 };
1608 input.context.subagents.set(input.callId, subagent);
1609
1610 if (existing === undefined) {
1611 yield* emitProviderEvent({
1612 type: "app_thread.created",
1613 driver: CursorAgentSdk.CURSOR_PROVIDER,
1614 appThread: makeSubagentChildThread({
1615 parentThread: input.context.input.appThread,
1616 childThreadId,
1617 parentNodeId: nodeId,
1618 activeProviderThreadId: null,
1619 providerInstanceId: input.context.input.modelSelection.instanceId,
1620 modelSelection: input.context.input.modelSelection,
1621 title: subagentThreadTitle({
1622 parentTitle: input.context.input.appThread.title,
1623 title: args.description,
1624 prompt: args.prompt,
1625 ordinal: input.context.subagents.size,
1626 }),
1627 now,
1628 createdBy: "agent",
1629 creationSource: "provider",
1630 }),
1631 });
1632 const promptNativeId = `${nativeItemId}:prompt`;
1633 const promptArtifacts = makeSubagentConversationArtifacts({
1634 senderThreadId: input.context.input.threadId,
1635 messageId: idAllocator.derive.messageFromProviderItem({
1636 driver: CursorAgentSdk.CURSOR_PROVIDER,
1637 nativeItemId: promptNativeId,
1638 }),
1639 turnItemId: idAllocator.derive.turnItemFromProviderItem({
1640 driver: CursorAgentSdk.CURSOR_PROVIDER,
1641 nativeItemId: promptNativeId,
1642 }),
1643 threadId: childThreadId,
1644 rootNodeId: childRootNodeId,
1645 providerThreadId: null,
1646 providerTurnId: null,
1647 nativeItemRef: {
1648 driver: CursorAgentSdk.CURSOR_PROVIDER,
1649 nativeId: promptNativeId,
1650 strength: "weak",
1651 },
1652 role: "user",
1653 text: args.prompt,
1654 ordinal: 100,
1655 now,
1656 });
1657 yield* emitProviderEvent({
1658 type: "message.updated",
1659 driver: CursorAgentSdk.CURSOR_PROVIDER,
1660 message: promptArtifacts.message,
1661 });
1662 yield* emitProviderEvent({
1663 type: "turn_item.updated",
1664 driver: CursorAgentSdk.CURSOR_PROVIDER,
1665 turnItem: promptArtifacts.turnItem,
1666 });
1667 }
1668
1669 yield* emitProviderEvent({
1670 type: "node.updated",
1671 driver: CursorAgentSdk.CURSOR_PROVIDER,
1672 node: {
1673 id: nodeId,
1674 threadId: input.context.input.threadId,
1675 runId: input.context.input.runId,
1676 parentNodeId: input.context.input.rootNodeId,
1677 rootNodeId: input.context.input.rootNodeId,
1678 kind: "subagent",
1679 status,
1680 countsForRun: false,
1681 providerThreadId: input.context.input.providerThread.id,
1682 providerTurnId: input.context.providerTurnId,
1683 nativeItemRef: task.nativeTaskRef,
1684 runtimeRequestId: null,
1685 checkpointScopeId: null,
1686 startedAt: task.startedAt,
1687 completedAt: task.completedAt,
1688 },
1689 });
1690 yield* emitProviderEvent({
1691 type: "node.updated",
1692 driver: CursorAgentSdk.CURSOR_PROVIDER,
1693 node: {
1694 id: childRootNodeId,
1695 threadId: childThreadId,
1696 runId: null,
1697 parentNodeId: null,
1698 rootNodeId: childRootNodeId,
1699 kind: "root_turn",
1700 status,
1701 countsForRun: false,
1702 providerThreadId: null,
1703 providerTurnId: null,
1704 nativeItemRef: task.nativeTaskRef,
1705 runtimeRequestId: null,
1706 checkpointScopeId: null,
1707 startedAt: task.startedAt,
1708 completedAt: task.completedAt,
1709 },
1710 });
1711 yield* emitProviderEvent({
1712 type: "subagent.updated",
1713 driver: CursorAgentSdk.CURSOR_PROVIDER,
1714 subagent: task,
1715 });
1716 yield* emitProviderEvent({
1717 type: "turn_item.updated",
1718 driver: CursorAgentSdk.CURSOR_PROVIDER,
1719 turnItem: {
1720 id: subagent.turnItemId,
1721 threadId: input.context.input.threadId,
1722 runId: input.context.input.runId,
1723 nodeId,
1724 providerThreadId: input.context.input.providerThread.id,
1725 providerTurnId: input.context.providerTurnId,
1726 nativeItemRef: task.nativeTaskRef,
1727 parentItemId: null,
1728 ordinal: subagent.turnItemOrdinal,
1729 status,
1730 title: task.title,
1731 startedAt: task.startedAt,
1732 completedAt: task.completedAt,
1733 updatedAt: now,
1734 type: "subagent",
1735 subagentId: task.id,
1736 origin: task.origin,
1737 driver: task.driver,
1738 providerInstanceId: task.providerInstanceId,
1739 childThreadId,
1740 prompt: task.prompt,
1741 result: task.result,
1742 },
1743 });
1744
1745 if (input.completed && !subagent.resultProjected) {
1746 for (const [index, nestedTool] of toolCallsFromConversationSteps(
1747 result?.conversationSteps ?? [],
1748 ).entries()) {
1749 const callId = `${nativeItemId}:child-tool:${nestedTool.callId || index + 1}`;
1750 const startedAt = task.startedAt ?? now;
1751 const active: ActiveCursorToolCall = {
1752 callId,
1753 toolCall: nestedTool.toolCall,
1754 target: {
1755 threadId: childThreadId,
1756 runId: null,
1757 rootNodeId: childRootNodeId,
1758 parentNodeId: childRootNodeId,
1759 providerThreadId: null,
1760 providerTurnId: null,
1761 },
1762 ordinal: ++subagent.nextChildOrdinal,
1763 startedAt,
1764 streamedOutput: "",
1765 };
1766 yield* emitToolArtifacts({ active, completed: true });
1767 }
1768 if (task.result !== null && task.result.length > 0) {
1769 const resultNativeId = `${nativeItemId}:result`;
1770 const resultArtifacts = makeSubagentConversationArtifacts({
1771 messageId: idAllocator.derive.messageFromProviderItem({
1772 driver: CursorAgentSdk.CURSOR_PROVIDER,
1773 nativeItemId: resultNativeId,
1774 }),
1775 turnItemId: idAllocator.derive.turnItemFromProviderItem({
1776 driver: CursorAgentSdk.CURSOR_PROVIDER,
1777 nativeItemId: resultNativeId,
1778 }),
1779 threadId: childThreadId,
1780 rootNodeId: childRootNodeId,
1781 providerThreadId: null,
1782 providerTurnId: null,
1783 nativeItemRef: {
1784 driver: CursorAgentSdk.CURSOR_PROVIDER,
1785 nativeId: resultNativeId,
1786 strength: "weak",
1787 },
1788 role: "assistant",
1789 text: task.result,
1790 ordinal: ++subagent.nextChildOrdinal,
1791 now,
1792 });
1793 yield* emitProviderEvent({
1794 type: "message.updated",
1795 driver: CursorAgentSdk.CURSOR_PROVIDER,
1796 message: resultArtifacts.message,
1797 });
1798 yield* emitProviderEvent({
1799 type: "turn_item.updated",
1800 driver: CursorAgentSdk.CURSOR_PROVIDER,
1801 turnItem: resultArtifacts.turnItem,
1802 });
1803 }
1804 subagent.resultProjected = true;
1805 }
1806 });
1807
1808 const handleToolUpdate = Effect.fnUntraced(function* (
1809 context: ActiveCursorTurn,
1810 update: Extract<
1811 InteractionUpdate,
1812 {
1813 readonly type: "tool-call-started" | "partial-tool-call" | "tool-call-completed";
1814 }
1815 >,
1816 ) {
1817 const completed = update.type === "tool-call-completed";
1818 const toolCall = update.toolCall;
1819 switch (toolCall.type) {
1820 case "createPlan":
1821 yield* emitPlanArtifacts({
1822 context,
1823 callId: update.callId,
1824 markdown: toolCall.args.plan,
1825 completed,
1826 failed: completed && cursorToolFailed(toolCall),
1827 });
1828 return;
1829 case "updateTodos":
1830 yield* emitTodoArtifacts({
1831 context,
1832 callId: update.callId,
1833 toolCall,
1834 completed,
1835 failed: completed && cursorToolFailed(toolCall),
1836 });
1837 return;
1838 case "task":
1839 if (update.type === "partial-tool-call") {
1840 return;
1841 }
1842 yield* emitSubagent({
1843 context,
1844 callId: update.callId,
1845 toolCall,
1846 completed,
1847 });
1848 return;
1849 default: {
1850 const active = yield* ensureToolStarted(context, update.callId, toolCall);
1851 active.toolCall = toolCall;
1852 if (update.type !== "tool-call-started") {
1853 yield* emitToolArtifacts({ active, completed });
1854 }
1855 if (completed) {
1856 context.tools.delete(update.callId);
1857 }
1858 }
1859 }
1860 });
1861
1862 const shellOutputText = (event: Record<string, unknown>): string => {
1863 const candidates = [
1864 event.text,
1865 event.output,
1866 event.data,
1867 event.stdout,
1868 event.stderr,
1869 event.chunk,
1870 ];
1871 return candidates.filter((value): value is string => typeof value === "string").join("");
1872 };
1873
1874 const handleInteractionUpdate = Effect.fnUntraced(function* (
1875 context: ActiveCursorTurn,
1876 update: InteractionUpdate,
1877 ) {
1878 if (context.finalized) {
1879 return;
1880 }
1881 switch (update.type) {
1882 case "text-delta":
1883 context.assistantReply.push(update.text);
1884 yield* completeReasoning(context);
1885 yield* appendTextSegment({
1886 context,
1887 stream: context.assistant,
1888 kind: "assistant",
1889 text: update.text,
1890 });
1891 yield* emitAssistant(context, false);
1892 return;
1893 case "thinking-delta":
1894 yield* completeAssistant(context);
1895 yield* appendTextSegment({
1896 context,
1897 stream: context.reasoning,
1898 kind: "reasoning",
1899 text: update.text,
1900 });
1901 yield* emitReasoning(context, false);
1902 return;
1903 case "thinking-completed":
1904 yield* completeReasoning(context);
1905 return;
1906 case "tool-call-started":
1907 case "partial-tool-call":
1908 case "tool-call-completed":
1909 yield* completeAssistant(context);
1910 yield* completeReasoning(context);
1911 yield* handleToolUpdate(context, update);
1912 return;
1913 case "step-completed":
1914 case "turn-ended":
1915 yield* completeAssistant(context);
1916 yield* completeReasoning(context);
1917 return;
1918 case "shell-output-delta": {
1919 const shell = Array.from(context.tools.values())
1920 .toReversed()
1921 .find((candidate) => candidate.toolCall.type === "shell");
1922 if (shell === undefined) {
1923 return;
1924 }
1925 shell.streamedOutput += shellOutputText(update.event);
1926 yield* emitToolArtifacts({ active: shell, completed: false });
1927 return;
1928 }
1929 default:
1930 return;
1931 }
1932 });
1933
1934 const providerTurnPayload = (input: {
1935 readonly context: ActiveCursorTurn;
1936 readonly status: OrchestrationV2ProviderTurn["status"];
1937 readonly completedAt: DateTime.Utc | null;
1938 }): OrchestrationV2ProviderTurn => ({
1939 id: input.context.providerTurnId,
1940 providerThreadId: input.context.input.providerThread.id,
1941 nodeId: input.context.input.rootNodeId,
1942 runAttemptId: input.context.input.attemptId,
1943 nativeTurnRef: {
1944 driver: CursorAgentSdk.CURSOR_PROVIDER,
1945 nativeId: input.context.run.runId,
1946 strength: "strong",
1947 },
1948 ordinal: input.context.input.providerTurnOrdinal,
1949 status: input.status,
1950 startedAt: input.context.startedAt,
1951 completedAt: input.completedAt,
1952 });
1953
1954 const finalizeTurn = Effect.fnUntraced(function* (input: {
1955 readonly context: ActiveCursorTurn;
1956 readonly status: Extract<
1957 OrchestrationV2ProviderTurn["status"],
1958 "completed" | "interrupted" | "failed" | "cancelled"
1959 >;
1960 readonly failure?: OrchestrationV2ProviderFailure;
1961 readonly threadDisposition?: "reusable" | "broken";
1962 }) {
1963 if (input.context.finalized) {
1964 return;
1965 }
1966 input.context.finalized = true;
1967 const completedAt = yield* DateTime.now;
1968 // Tools still here never got a tool-call-completed. A stopped or
1969 // failed turn cut them short, so they end with the turn's status.
1970 for (const tool of input.context.tools.values()) {
1971 yield* emitToolArtifacts({
1972 active: tool,
1973 completed: true,
1974 ...(input.status === "completed" ? {} : { unfinishedStatus: input.status }),
1975 });
1976 }
1977 input.context.tools.clear();
1978 // This interaction stops delivering updates at finalization. Tasks
1979 // without a completion have an unknown outcome. Background launch
1980 // acknowledgements have already settled their rows.
1981 for (const subagent of input.context.subagents.values()) {
1982 if (!isOrchestrationV2WorkActive(subagent.task.status)) continue;
1983 yield* emitSubagent({
1984 context: input.context,
1985 callId: subagent.callId,
1986 toolCall: subagent.toolCall,
1987 completed: true,
1988 status: input.status === "completed" ? "idle" : input.status,
1989 });
1990 }
1991 yield* completeReasoning(input.context);
1992 yield* completeAssistant(input.context);
1993 yield* emitProviderEvent({
1994 type: "provider_turn.updated",
1995 driver: CursorAgentSdk.CURSOR_PROVIDER,
1996 providerTurn: providerTurnPayload({
1997 context: input.context,
1998 status: input.status,
1999 completedAt,
2000 }),
2001 });
2002 yield* emitProviderEvent({
2003 type: "provider_thread.updated",
2004 driver: CursorAgentSdk.CURSOR_PROVIDER,
2005 providerThread: {
2006 ...input.context.input.providerThread,
2007 providerSessionId: session.id,
2008 status: "active",
2009 firstRunOrdinal:
2010 input.context.input.providerThread.firstRunOrdinal ??
2011 input.context.input.runOrdinal,
2012 lastRunOrdinal: input.context.input.runOrdinal,
2013 updatedAt: completedAt,
2014 },
2015 });
2016 const threadDisposition = input.threadDisposition ?? "reusable";
2017 yield* emitProviderEvent(
2018 input.status === "failed"
2019 ? {
2020 type: "turn.terminal",
2021 driver: CursorAgentSdk.CURSOR_PROVIDER,
2022 providerThreadId: input.context.input.providerThread.id,
2023 providerTurnId: input.context.providerTurnId,
2024 runOrdinal: input.context.input.runOrdinal,
2025 failureItemOrdinal: yield* resolveItemOrdinal(
2026 input.context,
2027 `terminal-failure:${input.context.providerTurnId}`,
2028 ),
2029 status: input.status,
2030 failure: input.failure ?? makeProviderFailure({ class: "provider_error" }),
2031 threadDisposition,
2032 }
2033 : {
2034 type: "turn.terminal",
2035 driver: CursorAgentSdk.CURSOR_PROVIDER,
2036 providerThreadId: input.context.input.providerThread.id,
2037 providerTurnId: input.context.providerTurnId,
2038 runOrdinal: input.context.input.runOrdinal,
2039 status: input.status,
2040 failure: null,
2041 threadDisposition,
2042 },
2043 );
2044 yield* Ref.update(activeTurn, (current) =>
2045 current?.providerTurnId === input.context.providerTurnId ? null : current,
2046 );
2047 yield* Deferred.succeed(input.context.completed, undefined);
2048 });
2049
2050 const terminalStatus = (
2051 context: ActiveCursorTurn,
2052 result: RunResult,
2053 ): Extract<
2054 OrchestrationV2ProviderTurn["status"],
2055 "completed" | "interrupted" | "failed" | "cancelled"
2056 > => {
2057 if (context.interrupted) {
2058 return "interrupted";
2059 }
2060 switch (result.status) {
2061 case "finished":
2062 return "completed";
2063 case "cancelled":
2064 return "cancelled";
2065 case "error":
2066 return "failed";
2067 }
2068 };
2069
2070 const openAgent = Effect.fnUntraced(function* (openInput: {
2071 readonly operation: "create" | "resume";
2072 readonly threadId: ThreadId;
2073 readonly modelSelection: ModelSelection;
2074 readonly runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy;
2075 readonly agentId?: string;
2076 }) {
2077 const existing = yield* Ref.get(liveAgent);
2078 if (
2079 existing !== null &&
2080 openInput.operation === "resume" &&
2081 existing.nativeThreadId === openInput.agentId
2082 ) {
2083 return existing;
2084 }
2085 if (existing !== null) {
2086 yield* existing.session.close.pipe(Effect.ignore);
2087 yield* Ref.set(liveAgent, null);
2088 }
2089 const sdkSession = yield* runner.open({
2090 operation: openInput.operation,
2091 ...(openInput.agentId === undefined ? {} : { agentId: openInput.agentId }),
2092 options: makeCursorAgentOptions({
2093 ...(apiKey === undefined ? {} : { apiKey }),
2094 modelSelection: openInput.modelSelection,
2095 runtimePolicy: openInput.runtimePolicy,
2096 threadId: openInput.threadId,
2097 mcpSession: yield* mcpSessions.read(openInput.threadId),
2098 }),
2099 threadId: openInput.threadId,
2100 providerSessionId: input.providerSessionId,
2101 });
2102 const next = {
2103 nativeThreadId: sdkSession.agentId,
2104 session: sdkSession,
2105 } satisfies CursorLiveAgent;
2106 yield* Ref.set(liveAgent, next);
2107 return next;
2108 });
2109
2110 let cursorSkillNames: ReadonlySet<string> | undefined;
2111 const resolveUserMessage = Effect.fnUntraced(function* (
2112 turnInput: ProviderAdapter.ProviderAdapterV2TurnInput,
2113 mcpServers: Record<string, McpServerConfig> | undefined,
2114 ) {
2115 const rawText = turnInput.message.text;
2116 if (rawText.trim() === "/compress" && turnInput.message.attachments.length === 0) {
2117 return "/compress";
2118 }
2119 if (hasCursorSkillMention(rawText) && cursorSkillNames === undefined) {
2120 const skills = yield* discoverCursorSkills(
2121 turnInput.runtimePolicy.cwd ?? undefined,
2122 adapterOptions.environment,
2123 ).pipe(
2124 Effect.provideService(FileSystem.FileSystem, fileSystem),
2125 Effect.provideService(Path.Path, path),
2126 );
2127 cursorSkillNames = new Set(
2128 skills
2129 .filter((skill) => skill.enabled && skill.userInvocable !== false)
2130 .map((skill) => skill.name),
2131 );
2132 }
2133 const userText = t3OrchestrationPromptForFirstRun({
2134 prompt: providerMessageTextWithAttachmentPaths({
2135 text:
2136 cursorSkillNames === undefined
2137 ? rawText
2138 : rewriteCursorSkillMentions(rawText, cursorSkillNames),
2139 attachments: turnInput.message.attachments,
2140 resolveAttachmentPath: host.resolveAttachmentPath,
2141 }),
2142 runOrdinal: turnInput.runOrdinal,
2143 hasT3Mcp: mcpServers !== undefined,
2144 });
2145 const images = yield* Effect.forEach(
2146 turnInput.message.attachments.filter(isProviderNativeImageAttachment),
2147 (attachment: ChatAttachment) =>
2148 Effect.gen(function* () {
2149 const path = host.resolveAttachmentPath(attachment);
2150 if (path === null) {
2151 return yield* new ProviderAdapter.ProviderAdapterProtocolError({
2152 driver: CursorAgentSdk.CURSOR_PROVIDER,
2153 detail: `Invalid attachment id '${attachment.id}'.`,
2154 });
2155 }
2156 const bytes = yield* fileSystem.readFile(path).pipe(
2157 Effect.mapError(
2158 (cause) =>
2159 new ProviderAdapter.ProviderAdapterProtocolError({
2160 driver: CursorAgentSdk.CURSOR_PROVIDER,
2161 detail: `Failed to read attachment '${attachment.id}'.`,
2162 payload: cause,
2163 }),
2164 ),
2165 );
2166 return {
2167 data: Buffer.from(bytes).toString("base64"),
2168 mimeType: attachment.mimeType,
2169 };
2170 }),
2171 { concurrency: 1 },
2172 );
2173 if (userText.length === 0 && images.length === 0) {
2174 return yield* new ProviderAdapter.ProviderAdapterProtocolError({
2175 driver: CursorAgentSdk.CURSOR_PROVIDER,
2176 detail: "Cursor turn requires non-empty text or attachments.",
2177 });
2178 }
2179 const text = `${userText}\n\n${buildRuntimeInstructions({ harness: "Cursor", model: turnInput.modelSelection.model })}`;
2180 return images.length === 0
2181 ? text
2182 : ({
2183 text,
2184 images,
2185 } satisfies SDKUserMessage);
2186 });
2187
2188 const startTurn = Effect.fn("CursorAdapterV2.startTurn")(
2189 function* (turnInput: ProviderAdapter.ProviderAdapterV2TurnInput) {
2190 const current = yield* Ref.get(activeTurn);
2191 if (current !== null) {
2192 return yield* new ProviderAdapter.ProviderAdapterProtocolError({
2193 driver: CursorAgentSdk.CURSOR_PROVIDER,
2194 detail: `Cursor provider turn ${current.providerTurnId} is still active.`,
2195 });
2196 }
2197 const agentId = nativeThreadId(turnInput.providerThread);
2198 const agent = yield* openAgent({
2199 operation: "resume",
2200 agentId,
2201 threadId: turnInput.threadId,
2202 modelSelection: turnInput.modelSelection,
2203 runtimePolicy: turnInput.runtimePolicy,
2204 });
2205 const mcpServers = cursorMcpServers(yield* mcpSessions.read(turnInput.threadId));
2206 const message = yield* resolveUserMessage(turnInput, mcpServers);
2207 const pendingUpdates: Array<InteractionUpdate> = [];
2208 let context: ActiveCursorTurn | null = null;
2209 // MCP send overrides replace the ambient servers loaded when the agent opens.
2210 const sdkRun = yield* agent.session.send({
2211 message,
2212 options: {
2213 model: cursorSdkModelSelection(turnInput.modelSelection),
2214 mode: turnInput.runtimePolicy.interactionMode === "plan" ? "plan" : "agent",
2215 },
2216 onDelta: (update) => {
2217 if (context === null) {
2218 return Effect.sync(() => {
2219 pendingUpdates.push(update);
2220 });
2221 }
2222 return handleInteractionUpdate(context, update);
2223 },
2224 });
2225 const startedAt = yield* DateTime.now;
2226 const completed = yield* Deferred.make<void, never>();
2227 const providerTurnId = idAllocator.derive.providerTurn({
2228 driver: CursorAgentSdk.CURSOR_PROVIDER,
2229 nativeTurnId: sdkRun.runId,
2230 });
2231 context = {
2232 input: turnInput,
2233 run: sdkRun,
2234 providerTurnId,
2235 startedAt,
2236 completed,
2237 itemOrdinals: new Map(),
2238 tools: new Map(),
2239 subagents: new Map(),
2240 assistant: {
2241 current: null,
2242 nextSegment: 0,
2243 },
2244 assistantReply: new CursorTransportFailure(),
2245 reasoning: {
2246 current: null,
2247 nextSegment: 0,
2248 },
2249 interrupted: false,
2250 finalized: false,
2251 };
2252 yield* Ref.set(activeTurn, context);
2253 yield* emitProviderEvent({
2254 type: "provider_turn.updated",
2255 driver: CursorAgentSdk.CURSOR_PROVIDER,
2256 providerTurn: providerTurnPayload({
2257 context,
2258 status: "running",
2259 completedAt: null,
2260 }),
2261 });
2262 yield* emitProviderEvent({
2263 type: "provider_thread.updated",
2264 driver: CursorAgentSdk.CURSOR_PROVIDER,
2265 providerThread: {
2266 ...turnInput.providerThread,
2267 providerSessionId: session.id,
2268 status: "active",
2269 updatedAt: startedAt,
2270 },
2271 });
2272 for (const update of pendingUpdates) {
2273 yield* handleInteractionUpdate(context, update);
2274 }
2275
2276 yield* sdkRun.wait.pipe(
2277 Effect.flatMap((result) =>
2278 Effect.gen(function* () {
2279 if (
2280 context !== null &&
2281 context.assistant.nextSegment === 0 &&
2282 result.result !== undefined &&
2283 result.result.length > 0
2284 ) {
2285 context.assistantReply.push(result.result);
2286 yield* appendTextSegment({
2287 context,
2288 stream: context.assistant,
2289 kind: "assistant",
2290 text: result.result,
2291 });
2292 }
2293 if (context !== null) {
2294 const transportFailure = context.assistantReply.failure;
2295 const status =
2296 transportFailure === undefined ? terminalStatus(context, result) : "failed";
2297 yield* finalizeTurn({
2298 context,
2299 status,
2300 ...(status === "failed"
2301 ? {
2302 failure: makeProviderFailure({
2303 cause:
2304 transportFailure ?? (result as { readonly error?: unknown }).error,
2305 class:
2306 transportFailure === undefined
2307 ? "provider_error"
2308 : "transport_error",
2309 }),
2310 }
2311 : {}),
2312 });
2313 }
2314 }),
2315 ),
2316 Effect.catch((cause) =>
2317 Effect.gen(function* () {
2318 if (context !== null) {
2319 yield* finalizeTurn({
2320 context,
2321 status: context.interrupted ? "interrupted" : "failed",
2322 ...(context.interrupted
2323 ? {}
2324 : {
2325 failure: makeProviderFailure({ cause, class: "transport_error" }),
2326 }),
2327 });
2328 }
2329 yield* Effect.logWarning("orchestration-v2.cursor-run-failed", {
2330 providerSessionId: input.providerSessionId,
2331 providerThreadId: turnInput.providerThread.id,
2332 providerTurnId,
2333 cause,
2334 });
2335 }),
2336 ),
2337 Effect.forkIn(sessionScope),
2338 );
2339 },
2340 (effect, turnInput) =>
2341 effect.pipe(
2342 Effect.mapError(
2343 (cause) =>
2344 new ProviderAdapter.ProviderAdapterTurnStartError({
2345 driver: CursorAgentSdk.CURSOR_PROVIDER,
2346 threadId: turnInput.threadId,
2347 providerThreadId: turnInput.providerThread.id,
2348 runId: turnInput.runId,
2349 cause,
2350 }),
2351 ),
2352 ),
2353 );
2354
2355 const closeSession = Effect.fnUntraced(function* () {
2356 const existing = yield* Ref.get(liveAgent);
2357 if (existing !== null) {
2358 yield* existing.session.close.pipe(Effect.ignore);
2359 yield* Ref.set(liveAgent, null);
2360 }
2361 yield* runner.assertComplete.pipe(
2362 Effect.catchCause((cause) =>
2363 Effect.logWarning("orchestration-v2.cursor-runner-incomplete", {
2364 providerSessionId: input.providerSessionId,
2365 cause,
2366 }),
2367 ),
2368 );
2369 });
2370 yield* Effect.addFinalizer(() => closeSession());
2371
2372 const runtime: ProviderAdapter.ProviderAdapterV2SessionRuntime = {
2373 instanceId: adapterOptions.instanceId,
2374 driver: CursorAgentSdk.CURSOR_PROVIDER,
2375 providerSessionId: input.providerSessionId,
2376 providerSession: session,
2377 events: Stream.fromEffectRepeat(Queue.take(events)),
2378 ensureThread: Effect.fn("CursorAdapterV2.ensureThread")(
2379 function* (threadInput: ProviderAdapter.ProviderAdapterV2EnsureThreadInput) {
2380 const opened = yield* openAgent({
2381 operation: "create",
2382 threadId: threadInput.threadId,
2383 modelSelection: threadInput.modelSelection,
2384 runtimePolicy: threadInput.runtimePolicy,
2385 });
2386 const now = yield* DateTime.now;
2387 return makeProviderThread({
2388 idAllocator,
2389 providerInstanceId: adapterOptions.instanceId,
2390 appThreadId: threadInput.threadId,
2391 providerSessionId: input.providerSessionId,
2392 nativeThreadId: opened.nativeThreadId,
2393 now,
2394 });
2395 },
2396 (effect, threadInput) =>
2397 effect.pipe(
2398 Effect.mapError(
2399 (cause) =>
2400 new ProviderAdapter.ProviderAdapterEnsureThreadError({
2401 driver: CursorAgentSdk.CURSOR_PROVIDER,
2402 threadId: threadInput.threadId,
2403 cause,
2404 }),
2405 ),
2406 ),
2407 ),
2408 resumeThread: Effect.fn("CursorAdapterV2.resumeThread")(
2409 function* (threadInput: { readonly providerThread: OrchestrationV2ProviderThread }) {
2410 const agentId = nativeThreadId(threadInput.providerThread);
2411 yield* openAgent({
2412 operation: "resume",
2413 agentId,
2414 threadId: threadInput.providerThread.appThreadId ?? input.threadId,
2415 modelSelection: input.modelSelection,
2416 runtimePolicy: input.runtimePolicy,
2417 });
2418 const now = yield* DateTime.now;
2419 return {
2420 ...threadInput.providerThread,
2421 providerSessionId: input.providerSessionId,
2422 status: "idle" as const,
2423 updatedAt: now,
2424 };
2425 },
2426 (effect, threadInput) =>
2427 effect.pipe(
2428 Effect.mapError(
2429 (cause) =>
2430 new ProviderAdapter.ProviderAdapterResumeThreadError({
2431 driver: CursorAgentSdk.CURSOR_PROVIDER,
2432 providerSessionId: input.providerSessionId,
2433 providerThreadId: threadInput.providerThread.id,
2434 cause,
2435 }),
2436 ),
2437 ),
2438 ),
2439 startTurn,
2440 compactThread: (turnInput) =>
2441 startTurn({
2442 ...turnInput,
2443 message: { ...turnInput.message, text: "/compress" },
2444 }),
2445 steerTurn: (turnInput) =>
2446 Effect.fail(
2447 new ProviderAdapter.ProviderAdapterSteerRunUnsupportedError({
2448 driver: CursorAgentSdk.CURSOR_PROVIDER,
2449 providerThreadId: turnInput.providerThread.id,
2450 }),
2451 ),
2452 interruptTurn: Effect.fn("CursorAdapterV2.interruptTurn")(
2453 function* (turnInput: ProviderAdapter.ProviderAdapterV2InterruptInput) {
2454 const context = yield* Ref.get(activeTurn);
2455 // Stop on a settled turn: finalization already ended its tools
2456 // and subagents, so nothing of it is left running to stop.
2457 if (context === null && turnInput.requestRuntimeRestart === true) return;
2458 if (context?.providerTurnId !== turnInput.providerTurnId) {
2459 return yield* new ProviderAdapter.ProviderAdapterProtocolError({
2460 driver: CursorAgentSdk.CURSOR_PROVIDER,
2461 detail: `Cursor provider turn ${turnInput.providerTurnId} is not active.`,
2462 });
2463 }
2464 context.interrupted = true;
2465 yield* context.run.cancel;
2466 const stopped = yield* Deferred.await(context.completed).pipe(
2467 Effect.timeoutOption("10 seconds"),
2468 );
2469 if (Option.isSome(stopped)) {
2470 return;
2471 }
2472 yield* Effect.logWarning("orchestration-v2.cursor-interrupt-timeout", {
2473 providerSessionId: input.providerSessionId,
2474 providerThreadId: turnInput.providerThread.id,
2475 providerTurnId: turnInput.providerTurnId,
2476 });
2477 yield* finalizeTurn({ context, status: "interrupted" });
2478 },
2479 (effect, turnInput) =>
2480 effect.pipe(
2481 Effect.mapError(
2482 (cause) =>
2483 new ProviderAdapter.ProviderAdapterInterruptError({
2484 driver: CursorAgentSdk.CURSOR_PROVIDER,
2485 providerThreadId: turnInput.providerThread.id,
2486 providerTurnId: turnInput.providerTurnId,
2487 cause,
2488 }),
2489 ),
2490 ),
2491 ),
2492 respondToRuntimeRequest: (requestInput) =>
2493 Effect.fail(
2494 new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({
2495 driver: CursorAgentSdk.CURSOR_PROVIDER,
2496 requestId: requestInput.requestId,
2497 cause: new ProviderAdapter.ProviderAdapterProtocolError({
2498 driver: CursorAgentSdk.CURSOR_PROVIDER,
2499 detail: "Cursor Agent SDK does not expose interactive approval requests.",
2500 }),
2501 }),
2502 ),
2503 readThreadSnapshot: Effect.fn("CursorAdapterV2.readThreadSnapshot")(
2504 function* (snapshotInput) {
2505 const requestedAgentId = nativeThreadId(snapshotInput.providerThread);
2506 const agent = yield* openAgent({
2507 operation: "resume",
2508 agentId: requestedAgentId,
2509 threadId: snapshotInput.providerThread.appThreadId ?? input.threadId,
2510 modelSelection: input.modelSelection,
2511 runtimePolicy: input.runtimePolicy,
2512 });
2513 const messages = yield* agent.session.listMessages;
2514 const now = yield* DateTime.now;
2515 const threadId = snapshotInput.providerThread.appThreadId ?? input.threadId;
2516 const projectedMessages: Array<OrchestrationV2ConversationMessage> = messages.flatMap(
2517 (message) => {
2518 const text = textFromAgentMessage(message);
2519 if (text.length === 0) {
2520 return [];
2521 }
2522 return [
2523 {
2524 createdBy: message.type === "user" ? "user" : "agent",
2525 creationSource: "provider",
2526 id: idAllocator.derive.messageFromProviderItem({
2527 driver: CursorAgentSdk.CURSOR_PROVIDER,
2528 nativeItemId: message.uuid,
2529 }),
2530 threadId,
2531 runId: null,
2532 nodeId: null,
2533 role: message.type,
2534 text,
2535 attachments: [],
2536 streaming: false,
2537 createdAt: now,
2538 updatedAt: now,
2539 },
2540 ];
2541 },
2542 );
2543 return {
2544 providerThread: {
2545 ...snapshotInput.providerThread,
2546 providerSessionId: input.providerSessionId,
2547 status: "idle" as const,
2548 updatedAt: now,
2549 },
2550 providerTurns: [],
2551 messages: projectedMessages,
2552 runtimeRequests: [],
2553 providerPayload: {
2554 agentId: requestedAgentId,
2555 messages,
2556 },
2557 };
2558 },
2559 (effect, snapshotInput) =>
2560 effect.pipe(
2561 Effect.mapError(
2562 (cause) =>
2563 new ProviderAdapter.ProviderAdapterReadThreadSnapshotError({
2564 driver: CursorAgentSdk.CURSOR_PROVIDER,
2565 providerThreadId: snapshotInput.providerThread.id,
2566 cause,
2567 }),
2568 ),
2569 ),
2570 ),
2571 rollbackThread: (rollbackInput) =>
2572 Effect.fail(
2573 new ProviderAdapter.ProviderAdapterRollbackThreadError({
2574 driver: CursorAgentSdk.CURSOR_PROVIDER,
2575 providerThreadId: rollbackInput.providerThread.id,
2576 checkpointId: rollbackInput.target.checkpointId,
2577 cause: "Cursor Agent SDK does not expose conversation rollback.",
2578 }),
2579 ),
2580 forkThread: (forkInput) =>
2581 Effect.fail(
2582 new ProviderAdapter.ProviderAdapterForkThreadError({
2583 driver: CursorAgentSdk.CURSOR_PROVIDER,
2584 providerThreadId: forkInput.sourceProviderThread.id,
2585 cause: "Cursor Agent SDK does not expose native agent forks.",
2586 }),
2587 ),
2588 };
2589 return runtime;
2590 },
2591 (effect, input) =>
2592 effect.pipe(
2593 Effect.mapError(
2594 (cause) =>
2595 new ProviderAdapter.ProviderAdapterOpenSessionError({
2596 driver: CursorAgentSdk.CURSOR_PROVIDER,
2597 providerSessionId: input.providerSessionId,
2598 cause,
2599 }),
2600 ),
2601 ),
2602 ),
2603 });
2604});
2605
2606export type CursorAdapterV2DriverEnv =
2607 | CursorAgentSdk.CursorAgentSdkRunner
2608 | FileSystem.FileSystem
2609 | Path.Path
2610 | IdAllocator.IdAllocatorV2
2611 | McpProviderSessions.McpProviderSessions
2612 | ProviderHost.ProviderHost;
2613
2614export const CursorAdapterV2Driver: ProviderAdapterDriver<
2615 CursorSettings,
2616 CursorAdapterV2DriverEnv
2617> = {
2618 driverKind: CURSOR_DRIVER_KIND,
2619 configSchema: CursorSettings,
2620 defaultConfig: (): CursorSettings => DEFAULT_CURSOR_SETTINGS,
2621 create: Effect.fn("CursorAdapterV2Driver.create")(
2622 function* (input: ProviderAdapterDriverCreateInput<CursorSettings>) {
2623 const hostEnvironment = yield* HostProcess.Environment;
2624 return yield* makeCursorAdapterV2({
2625 instanceId: input.instanceId,
2626 settings: {
2627 ...input.config,
2628 enabled: input.enabled,
2629 },
2630 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
2631 });
2632 },
2633 (effect, input) =>
2634 effect.pipe(
2635 Effect.mapError(
2636 (cause) =>
2637 new ProviderAdapterDriverCreateError({
2638 driver: CURSOR_DRIVER_KIND,
2639 instanceId: input.instanceId,
2640 detail: "Failed to create Cursor Agent SDK adapter.",
2641 cause,
2642 }),
2643 ),
2644 ),
2645 ),
2646};