@oh-my-pi/pi-ai 4.2.2 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1998 @@
1
+ import { createHash } from "node:crypto";
2
+ import { appendFile } from "node:fs/promises";
3
+ import http2 from "node:http2";
4
+ import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf";
5
+ import { ValueSchema } from "@bufbuild/protobuf/wkt";
6
+ import JSON5 from "json5";
7
+ import { calculateCost } from "../models";
8
+ import type {
9
+ Api,
10
+ AssistantMessage,
11
+ Context,
12
+ CursorExecHandlerResult,
13
+ CursorExecHandlers,
14
+ CursorMcpCall,
15
+ CursorToolResultHandler,
16
+ ImageContent,
17
+ Message,
18
+ Model,
19
+ StreamFunction,
20
+ StreamOptions,
21
+ TextContent,
22
+ ThinkingContent,
23
+ Tool,
24
+ ToolCall,
25
+ ToolResultMessage,
26
+ } from "../types";
27
+ import { AssistantMessageEventStream } from "../utils/event-stream";
28
+ import { parseStreamingJson } from "../utils/json-parse";
29
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
30
+ import type { McpToolDefinition } from "./cursor/gen/agent_pb";
31
+ import {
32
+ AgentClientMessageSchema,
33
+ AgentConversationTurnStructureSchema,
34
+ AgentRunRequestSchema,
35
+ type AgentServerMessage,
36
+ AgentServerMessageSchema,
37
+ AssistantMessageSchema,
38
+ BackgroundShellSpawnResultSchema,
39
+ ClientHeartbeatSchema,
40
+ ConversationActionSchema,
41
+ type ConversationStateStructure,
42
+ ConversationStateStructureSchema,
43
+ ConversationStepSchema,
44
+ ConversationTurnStructureSchema,
45
+ DeleteErrorSchema,
46
+ DeleteRejectedSchema,
47
+ DeleteResultSchema,
48
+ DeleteSuccessSchema,
49
+ DiagnosticsErrorSchema,
50
+ DiagnosticsRejectedSchema,
51
+ DiagnosticsResultSchema,
52
+ DiagnosticsSuccessSchema,
53
+ type ExecClientMessage,
54
+ ExecClientMessageSchema,
55
+ type ExecServerMessage,
56
+ FetchErrorSchema,
57
+ FetchResultSchema,
58
+ GetBlobResultSchema,
59
+ GrepContentMatchSchema,
60
+ GrepContentResultSchema,
61
+ GrepCountResultSchema,
62
+ GrepErrorSchema,
63
+ type GrepFileCount,
64
+ GrepFileCountSchema,
65
+ GrepFileMatchSchema,
66
+ GrepFilesResultSchema,
67
+ GrepResultSchema,
68
+ GrepSuccessSchema,
69
+ type GrepUnionResult,
70
+ GrepUnionResultSchema,
71
+ KvClientMessageSchema,
72
+ type KvServerMessage,
73
+ type LsDirectoryTreeNode,
74
+ type LsDirectoryTreeNode_File,
75
+ LsDirectoryTreeNode_FileSchema,
76
+ LsDirectoryTreeNodeSchema,
77
+ LsErrorSchema,
78
+ LsRejectedSchema,
79
+ LsResultSchema,
80
+ LsSuccessSchema,
81
+ McpErrorSchema,
82
+ McpImageContentSchema,
83
+ McpResultSchema,
84
+ McpSuccessSchema,
85
+ McpTextContentSchema,
86
+ McpToolDefinitionSchema,
87
+ McpToolNotFoundSchema,
88
+ McpToolResultContentItemSchema,
89
+ ModelDetailsSchema,
90
+ ReadErrorSchema,
91
+ ReadRejectedSchema,
92
+ ReadResultSchema,
93
+ ReadSuccessSchema,
94
+ RequestContextResultSchema,
95
+ RequestContextSchema,
96
+ RequestContextSuccessSchema,
97
+ SetBlobResultSchema,
98
+ type ShellArgs,
99
+ ShellFailureSchema,
100
+ ShellRejectedSchema,
101
+ ShellResultSchema,
102
+ type ShellStream,
103
+ ShellStreamExitSchema,
104
+ ShellStreamSchema,
105
+ ShellStreamStartSchema,
106
+ ShellStreamStderrSchema,
107
+ ShellStreamStdoutSchema,
108
+ ShellSuccessSchema,
109
+ UserMessageActionSchema,
110
+ UserMessageSchema,
111
+ WriteErrorSchema,
112
+ WriteRejectedSchema,
113
+ WriteResultSchema,
114
+ WriteShellStdinErrorSchema,
115
+ WriteShellStdinResultSchema,
116
+ WriteSuccessSchema,
117
+ } from "./cursor/gen/agent_pb";
118
+
119
+ export const CURSOR_API_URL = "https://api2.cursor.sh";
120
+ export const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
121
+
122
+ const conversationStateCache = new Map<string, ConversationStateStructure>();
123
+ const conversationBlobStores = new Map<string, Map<string, Uint8Array>>();
124
+
125
+ export interface CursorOptions extends StreamOptions {
126
+ customSystemPrompt?: string;
127
+ conversationId?: string;
128
+ execHandlers?: CursorExecHandlers;
129
+ onToolResult?: CursorToolResultHandler;
130
+ }
131
+
132
+ const CONNECT_END_STREAM_FLAG = 0b00000010;
133
+
134
+ interface CursorLogEntry {
135
+ ts: number;
136
+ type: string;
137
+ subtype?: string;
138
+ data?: unknown;
139
+ }
140
+
141
+ async function appendCursorDebugLog(entry: CursorLogEntry): Promise<void> {
142
+ const logPath = process.env.DEBUG_CURSOR_LOG;
143
+ if (!logPath) return;
144
+ try {
145
+ await appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`);
146
+ } catch {
147
+ // Ignore debug log failures
148
+ }
149
+ }
150
+
151
+ function log(type: string, subtype?: string, data?: unknown): void {
152
+ if (!process.env.DEBUG_CURSOR) return;
153
+ const normalizedData = data ? decodeLogData(data) : data;
154
+ const entry: CursorLogEntry = { ts: Date.now(), type, subtype, data: normalizedData };
155
+ const verbose = process.env.DEBUG_CURSOR === "2" || process.env.DEBUG_CURSOR === "verbose";
156
+ const dataStr = verbose && normalizedData ? ` ${JSON.stringify(normalizedData, debugReplacer)?.slice(0, 500)}` : "";
157
+ console.error(`[CURSOR] ${type}${subtype ? `: ${subtype}` : ""}${dataStr}`);
158
+ void appendCursorDebugLog(entry);
159
+ }
160
+
161
+ function frameConnectMessage(data: Uint8Array, flags = 0): Buffer {
162
+ const frame = Buffer.alloc(5 + data.length);
163
+ frame[0] = flags;
164
+ frame.writeUInt32BE(data.length, 1);
165
+ frame.set(data, 5);
166
+ return frame;
167
+ }
168
+
169
+ function parseConnectEndStream(data: Uint8Array): Error | null {
170
+ try {
171
+ const payload = JSON.parse(new TextDecoder().decode(data));
172
+ const error = payload?.error;
173
+ if (error) {
174
+ const code = typeof error.code === "string" ? error.code : "unknown";
175
+ const message = typeof error.message === "string" ? error.message : "Unknown error";
176
+ return new Error(`Connect error ${code}: ${message}`);
177
+ }
178
+ return null;
179
+ } catch {
180
+ return new Error("Failed to parse Connect end stream");
181
+ }
182
+ }
183
+
184
+ function debugBytes(bytes: Uint8Array, asHex: boolean): string {
185
+ if (asHex) {
186
+ return Buffer.from(bytes).toString("hex");
187
+ }
188
+ try {
189
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
190
+ if (/^[\x20-\x7E\s]*$/.test(text)) return text;
191
+ } catch {}
192
+ return Buffer.from(bytes).toString("hex");
193
+ }
194
+
195
+ function debugReplacer(key: string, value: unknown): unknown {
196
+ if (
197
+ value instanceof Uint8Array ||
198
+ (value && typeof value === "object" && "type" in value && value.type === "Buffer")
199
+ ) {
200
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array((value as any).data);
201
+ const asHex = key === "blobId" || key === "blob_id" || key.endsWith("Id") || key.endsWith("_id");
202
+ return debugBytes(bytes, asHex);
203
+ }
204
+ if (typeof value === "bigint") return value.toString();
205
+ return value;
206
+ }
207
+
208
+ function extractLogBytes(value: unknown): Uint8Array | null {
209
+ if (value instanceof Uint8Array) {
210
+ return value;
211
+ }
212
+ if (value && typeof value === "object" && "type" in value && value.type === "Buffer") {
213
+ const data = (value as { data?: number[] }).data;
214
+ if (Array.isArray(data)) {
215
+ return new Uint8Array(data);
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+
221
+ function decodeMcpArgsForLog(args?: Record<string, unknown>): Record<string, unknown> | undefined {
222
+ if (!args) {
223
+ return undefined;
224
+ }
225
+ let mutated = false;
226
+ const decoded: Record<string, unknown> = {};
227
+ for (const [key, value] of Object.entries(args)) {
228
+ const bytes = extractLogBytes(value);
229
+ if (bytes) {
230
+ decoded[key] = decodeMcpArgValue(bytes);
231
+ mutated = true;
232
+ continue;
233
+ }
234
+ const normalizedValue = decodeLogData(value);
235
+ decoded[key] = normalizedValue;
236
+ if (normalizedValue !== value) {
237
+ mutated = true;
238
+ }
239
+ }
240
+ return mutated ? decoded : args;
241
+ }
242
+
243
+ function decodeLogData(value: unknown): unknown {
244
+ if (!value || typeof value !== "object") {
245
+ return value;
246
+ }
247
+ if (Array.isArray(value)) {
248
+ return value.map((entry) => decodeLogData(entry));
249
+ }
250
+ const record = value as Record<string, unknown>;
251
+ const typeName = record.$typeName;
252
+ const stripTypeName = typeof typeName === "string" && typeName.startsWith("agent.v1.");
253
+
254
+ if (typeName === "agent.v1.McpArgs") {
255
+ const decodedArgs = decodeMcpArgsForLog(record.args as Record<string, unknown> | undefined);
256
+ const base = stripTypeName ? omitTypeName(record) : record;
257
+ return decodedArgs ? { ...base, args: decodedArgs } : base;
258
+ }
259
+ if (typeName === "agent.v1.McpToolCall") {
260
+ const argsRecord = record.args as Record<string, unknown> | undefined;
261
+ const decodedArgs = decodeMcpArgsForLog(argsRecord?.args as Record<string, unknown> | undefined);
262
+ const base = stripTypeName ? omitTypeName(record) : record;
263
+ if (decodedArgs && argsRecord) {
264
+ return { ...base, args: { ...argsRecord, args: decodedArgs } };
265
+ }
266
+ return base;
267
+ }
268
+
269
+ let mutated = stripTypeName;
270
+ const decoded: Record<string, unknown> = {};
271
+ for (const [key, entry] of Object.entries(record)) {
272
+ if (stripTypeName && key === "$typeName") {
273
+ continue;
274
+ }
275
+ const normalizedEntry = decodeLogData(entry);
276
+ decoded[key] = normalizedEntry;
277
+ if (normalizedEntry !== entry) {
278
+ mutated = true;
279
+ }
280
+ }
281
+ return mutated ? decoded : record;
282
+ }
283
+
284
+ function omitTypeName(record: Record<string, unknown>): Record<string, unknown> {
285
+ const { $typeName: _, ...rest } = record;
286
+ return rest;
287
+ }
288
+
289
+ export const streamCursor: StreamFunction<"cursor-agent"> = (
290
+ model: Model<"cursor-agent">,
291
+ context: Context,
292
+ options?: CursorOptions,
293
+ ): AssistantMessageEventStream => {
294
+ const stream = new AssistantMessageEventStream();
295
+
296
+ (async () => {
297
+ const output: AssistantMessage = {
298
+ role: "assistant",
299
+ content: [],
300
+ api: "cursor-agent" as Api,
301
+ provider: model.provider,
302
+ model: model.id,
303
+ usage: {
304
+ input: 0,
305
+ output: 0,
306
+ cacheRead: 0,
307
+ cacheWrite: 0,
308
+ totalTokens: 0,
309
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
310
+ },
311
+ stopReason: "stop",
312
+ timestamp: Date.now(),
313
+ };
314
+
315
+ let h2Client: http2.ClientHttp2Session | null = null;
316
+ let h2Request: http2.ClientHttp2Stream | null = null;
317
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
318
+
319
+ try {
320
+ const apiKey = options?.apiKey;
321
+ if (!apiKey) {
322
+ throw new Error("Cursor API key (access token) is required");
323
+ }
324
+
325
+ const conversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
326
+ const blobStore = conversationBlobStores.get(conversationId) ?? new Map<string, Uint8Array>();
327
+ conversationBlobStores.set(conversationId, blobStore);
328
+ const cachedState = conversationStateCache.get(conversationId);
329
+ const { requestBytes, conversationState } = buildGrpcRequest(model, context, options, {
330
+ conversationId,
331
+ blobStore,
332
+ conversationState: cachedState,
333
+ });
334
+ conversationStateCache.set(conversationId, conversationState);
335
+ const requestContextTools = buildMcpToolDefinitions(context.tools);
336
+
337
+ const baseUrl = model.baseUrl || CURSOR_API_URL;
338
+ h2Client = http2.connect(baseUrl);
339
+
340
+ h2Request = h2Client.request({
341
+ ":method": "POST",
342
+ ":path": "/agent.v1.AgentService/Run",
343
+ "content-type": "application/connect+proto",
344
+ "connect-protocol-version": "1",
345
+ te: "trailers",
346
+ authorization: `Bearer ${apiKey}`,
347
+ "x-ghost-mode": "true",
348
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
349
+ "x-cursor-client-type": "cli",
350
+ "x-request-id": crypto.randomUUID(),
351
+ });
352
+
353
+ stream.push({ type: "start", partial: output });
354
+
355
+ let pendingBuffer = Buffer.alloc(0);
356
+ let endStreamError: Error | null = null;
357
+ let currentTextBlock: (TextContent & { index: number }) | null = null;
358
+ let currentThinkingBlock: (ThinkingContent & { index: number }) | null = null;
359
+ let currentToolCall: (ToolCall & { index: number; partialJson: string }) | null = null;
360
+ const usageState: UsageState = { sawTokenDelta: false };
361
+
362
+ const state: BlockState = {
363
+ get currentTextBlock() {
364
+ return currentTextBlock;
365
+ },
366
+ get currentThinkingBlock() {
367
+ return currentThinkingBlock;
368
+ },
369
+ get currentToolCall() {
370
+ return currentToolCall;
371
+ },
372
+ setTextBlock: (b) => {
373
+ currentTextBlock = b;
374
+ },
375
+ setThinkingBlock: (b) => {
376
+ currentThinkingBlock = b;
377
+ },
378
+ setToolCall: (t) => {
379
+ currentToolCall = t;
380
+ },
381
+ };
382
+
383
+ const onConversationCheckpoint = (checkpoint: ConversationStateStructure) => {
384
+ conversationStateCache.set(conversationId, checkpoint);
385
+ };
386
+
387
+ h2Request.on("data", (chunk: Buffer) => {
388
+ pendingBuffer = Buffer.concat([pendingBuffer, chunk]);
389
+
390
+ while (pendingBuffer.length >= 5) {
391
+ const flags = pendingBuffer[0];
392
+ const msgLen = pendingBuffer.readUInt32BE(1);
393
+ if (pendingBuffer.length < 5 + msgLen) break;
394
+
395
+ const messageBytes = pendingBuffer.subarray(5, 5 + msgLen);
396
+ pendingBuffer = pendingBuffer.subarray(5 + msgLen);
397
+
398
+ if (flags & CONNECT_END_STREAM_FLAG) {
399
+ const endError = parseConnectEndStream(messageBytes);
400
+ if (endError) {
401
+ endStreamError = endError;
402
+ h2Request?.close();
403
+ }
404
+ continue;
405
+ }
406
+
407
+ try {
408
+ const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
409
+ void handleServerMessage(
410
+ serverMessage,
411
+ output,
412
+ stream,
413
+ state,
414
+ blobStore,
415
+ h2Request!,
416
+ options?.execHandlers,
417
+ options?.onToolResult,
418
+ usageState,
419
+ requestContextTools,
420
+ onConversationCheckpoint,
421
+ ).catch((error) => {
422
+ log("error", "handleServerMessage", { error: String(error) });
423
+ });
424
+ } catch (e) {
425
+ log("error", "parseServerMessage", { error: String(e) });
426
+ }
427
+ }
428
+ });
429
+
430
+ h2Request.write(frameConnectMessage(requestBytes));
431
+
432
+ const sendHeartbeat = () => {
433
+ if (!h2Request || h2Request.closed) {
434
+ return;
435
+ }
436
+ const heartbeatMessage = create(AgentClientMessageSchema, {
437
+ message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
438
+ });
439
+ const heartbeatBytes = toBinary(AgentClientMessageSchema, heartbeatMessage);
440
+ h2Request.write(frameConnectMessage(heartbeatBytes));
441
+ };
442
+
443
+ heartbeatTimer = setInterval(sendHeartbeat, 5000);
444
+
445
+ await new Promise<void>((resolve, reject) => {
446
+ h2Request!.on("trailers", (trailers) => {
447
+ const status = trailers["grpc-status"];
448
+ const msg = trailers["grpc-message"];
449
+ if (status && status !== "0") {
450
+ reject(new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`));
451
+ }
452
+ });
453
+
454
+ h2Request!.on("end", () => {
455
+ if (endStreamError) {
456
+ reject(endStreamError);
457
+ return;
458
+ }
459
+ resolve();
460
+ });
461
+
462
+ h2Request!.on("error", reject);
463
+
464
+ if (options?.signal) {
465
+ options.signal.addEventListener("abort", () => {
466
+ h2Request?.close();
467
+ reject(new Error("Request was aborted"));
468
+ });
469
+ }
470
+ });
471
+
472
+ if (state.currentTextBlock) {
473
+ const idx = output.content.indexOf(state.currentTextBlock);
474
+ stream.push({
475
+ type: "text_end",
476
+ contentIndex: idx,
477
+ content: state.currentTextBlock.text,
478
+ partial: output,
479
+ });
480
+ }
481
+ if (state.currentThinkingBlock) {
482
+ const idx = output.content.indexOf(state.currentThinkingBlock);
483
+ stream.push({
484
+ type: "thinking_end",
485
+ contentIndex: idx,
486
+ content: state.currentThinkingBlock.thinking,
487
+ partial: output,
488
+ });
489
+ }
490
+ if (state.currentToolCall) {
491
+ const idx = output.content.indexOf(state.currentToolCall);
492
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
493
+ delete (state.currentToolCall as any).partialJson;
494
+ delete (state.currentToolCall as any).index;
495
+ stream.push({
496
+ type: "toolcall_end",
497
+ contentIndex: idx,
498
+ toolCall: state.currentToolCall,
499
+ partial: output,
500
+ });
501
+ }
502
+
503
+ calculateCost(model, output.usage);
504
+
505
+ stream.push({
506
+ type: "done",
507
+ reason: output.stopReason as "stop" | "length" | "toolUse",
508
+ message: output,
509
+ });
510
+ stream.end();
511
+ } catch (error) {
512
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
513
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
514
+ stream.push({ type: "error", reason: output.stopReason, error: output });
515
+ stream.end();
516
+ } finally {
517
+ if (heartbeatTimer) {
518
+ clearInterval(heartbeatTimer);
519
+ heartbeatTimer = null;
520
+ }
521
+ h2Request?.close();
522
+ h2Client?.close();
523
+ }
524
+ })();
525
+
526
+ return stream;
527
+ };
528
+
529
+ interface BlockState {
530
+ currentTextBlock: (TextContent & { index: number }) | null;
531
+ currentThinkingBlock: (ThinkingContent & { index: number }) | null;
532
+ currentToolCall: (ToolCall & { index: number; partialJson: string }) | null;
533
+ setTextBlock: (b: (TextContent & { index: number }) | null) => void;
534
+ setThinkingBlock: (b: (ThinkingContent & { index: number }) | null) => void;
535
+ setToolCall: (t: (ToolCall & { index: number; partialJson: string }) | null) => void;
536
+ }
537
+
538
+ interface UsageState {
539
+ sawTokenDelta: boolean;
540
+ }
541
+
542
+ async function handleServerMessage(
543
+ msg: AgentServerMessage,
544
+ output: AssistantMessage,
545
+ stream: AssistantMessageEventStream,
546
+ state: BlockState,
547
+ blobStore: Map<string, Uint8Array>,
548
+ h2Request: http2.ClientHttp2Stream,
549
+ execHandlers: CursorExecHandlers | undefined,
550
+ onToolResult: CursorToolResultHandler | undefined,
551
+ usageState: UsageState,
552
+ requestContextTools: McpToolDefinition[],
553
+ onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
554
+ ): Promise<void> {
555
+ const msgCase = msg.message.case;
556
+
557
+ log("serverMessage", msgCase, msg.message.value);
558
+
559
+ if (msgCase === "interactionUpdate") {
560
+ processInteractionUpdate(msg.message.value, output, stream, state, usageState);
561
+ } else if (msgCase === "kvServerMessage") {
562
+ handleKvServerMessage(msg.message.value as KvServerMessage, blobStore, h2Request);
563
+ } else if (msgCase === "execServerMessage") {
564
+ await handleExecServerMessage(
565
+ msg.message.value as ExecServerMessage,
566
+ h2Request,
567
+ execHandlers,
568
+ onToolResult,
569
+ requestContextTools,
570
+ );
571
+ } else if (msgCase === "conversationCheckpointUpdate") {
572
+ handleConversationCheckpointUpdate(msg.message.value, output, usageState, onConversationCheckpoint);
573
+ }
574
+ }
575
+
576
+ function handleKvServerMessage(
577
+ kvMsg: KvServerMessage,
578
+ blobStore: Map<string, Uint8Array>,
579
+ h2Request: http2.ClientHttp2Stream,
580
+ ): void {
581
+ const kvCase = kvMsg.message.case;
582
+
583
+ if (kvCase === "getBlobArgs") {
584
+ const blobId = kvMsg.message.value.blobId;
585
+ const blobIdKey = Buffer.from(blobId).toString("hex");
586
+
587
+ const blobData = blobStore.get(blobIdKey);
588
+
589
+ const response = create(KvClientMessageSchema, {
590
+ id: kvMsg.id,
591
+ message: {
592
+ case: "getBlobResult",
593
+ value: create(GetBlobResultSchema, blobData ? { blobData } : {}),
594
+ },
595
+ });
596
+
597
+ const kvClientMessage = create(AgentClientMessageSchema, {
598
+ message: { case: "kvClientMessage", value: response },
599
+ });
600
+
601
+ const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
602
+ h2Request.write(frameConnectMessage(responseBytes));
603
+
604
+ log("kvClient", "getBlobResult", { blobId: blobIdKey.slice(0, 40) });
605
+ } else if (kvCase === "setBlobArgs") {
606
+ const { blobId, blobData } = kvMsg.message.value;
607
+ const blobIdKey = Buffer.from(blobId).toString("hex");
608
+ blobStore.set(blobIdKey, blobData);
609
+
610
+ const response = create(KvClientMessageSchema, {
611
+ id: kvMsg.id,
612
+ message: {
613
+ case: "setBlobResult",
614
+ value: create(SetBlobResultSchema, {}),
615
+ },
616
+ });
617
+
618
+ const kvClientMessage = create(AgentClientMessageSchema, {
619
+ message: { case: "kvClientMessage", value: response },
620
+ });
621
+
622
+ const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
623
+ h2Request.write(frameConnectMessage(responseBytes));
624
+
625
+ log("kvClient", "setBlobResult", { blobId: blobIdKey.slice(0, 40) });
626
+ }
627
+ }
628
+
629
+ function sendShellStreamEvent(
630
+ h2Request: http2.ClientHttp2Stream,
631
+ execMsg: ExecServerMessage,
632
+ event: ShellStream["event"],
633
+ ): void {
634
+ sendExecClientMessage(h2Request, execMsg, "shellStream", create(ShellStreamSchema, { event }));
635
+ }
636
+
637
+ async function handleShellStreamArgs(
638
+ args: ShellArgs,
639
+ execMsg: ExecServerMessage,
640
+ h2Request: http2.ClientHttp2Stream,
641
+ execHandlers: CursorExecHandlers | undefined,
642
+ onToolResult: CursorToolResultHandler | undefined,
643
+ ): Promise<void> {
644
+ const { execResult } = await resolveExecHandler(
645
+ args as any,
646
+ execHandlers?.shell,
647
+ onToolResult,
648
+ (toolResult) => buildShellResultFromToolResult(args as any, toolResult),
649
+ (reason) => buildShellRejectedResult((args as any).command, (args as any).workingDirectory, reason),
650
+ (error) => buildShellFailureResult((args as any).command, (args as any).workingDirectory, error),
651
+ );
652
+
653
+ sendShellStreamEvent(h2Request, execMsg, { case: "start", value: create(ShellStreamStartSchema, {}) });
654
+
655
+ const result = execResult.result;
656
+ switch (result.case) {
657
+ case "success": {
658
+ const value = result.value;
659
+ if (value.stdout) {
660
+ sendShellStreamEvent(h2Request, execMsg, {
661
+ case: "stdout",
662
+ value: create(ShellStreamStdoutSchema, { data: value.stdout }),
663
+ });
664
+ }
665
+ if (value.stderr) {
666
+ sendShellStreamEvent(h2Request, execMsg, {
667
+ case: "stderr",
668
+ value: create(ShellStreamStderrSchema, { data: value.stderr }),
669
+ });
670
+ }
671
+ sendShellStreamEvent(h2Request, execMsg, {
672
+ case: "exit",
673
+ value: create(ShellStreamExitSchema, {
674
+ code: value.exitCode,
675
+ cwd: value.workingDirectory,
676
+ aborted: false,
677
+ }),
678
+ });
679
+ return;
680
+ }
681
+ case "failure": {
682
+ const value = result.value;
683
+ if (value.stdout) {
684
+ sendShellStreamEvent(h2Request, execMsg, {
685
+ case: "stdout",
686
+ value: create(ShellStreamStdoutSchema, { data: value.stdout }),
687
+ });
688
+ }
689
+ if (value.stderr) {
690
+ sendShellStreamEvent(h2Request, execMsg, {
691
+ case: "stderr",
692
+ value: create(ShellStreamStderrSchema, { data: value.stderr }),
693
+ });
694
+ }
695
+ sendShellStreamEvent(h2Request, execMsg, {
696
+ case: "exit",
697
+ value: create(ShellStreamExitSchema, {
698
+ code: value.exitCode,
699
+ cwd: value.workingDirectory,
700
+ aborted: value.aborted,
701
+ abortReason: value.abortReason,
702
+ }),
703
+ });
704
+ return;
705
+ }
706
+ case "rejected": {
707
+ sendShellStreamEvent(h2Request, execMsg, { case: "rejected", value: result.value });
708
+ sendShellStreamEvent(h2Request, execMsg, {
709
+ case: "exit",
710
+ value: create(ShellStreamExitSchema, {
711
+ code: 1,
712
+ cwd: result.value.workingDirectory,
713
+ aborted: false,
714
+ }),
715
+ });
716
+ return;
717
+ }
718
+ case "timeout": {
719
+ const value = result.value;
720
+ sendShellStreamEvent(h2Request, execMsg, {
721
+ case: "stderr",
722
+ value: create(ShellStreamStderrSchema, {
723
+ data: `Command timed out after ${value.timeoutMs}ms`,
724
+ }),
725
+ });
726
+ sendShellStreamEvent(h2Request, execMsg, {
727
+ case: "exit",
728
+ value: create(ShellStreamExitSchema, {
729
+ code: 1,
730
+ cwd: value.workingDirectory,
731
+ aborted: true,
732
+ }),
733
+ });
734
+ return;
735
+ }
736
+ case "permissionDenied": {
737
+ sendShellStreamEvent(h2Request, execMsg, { case: "permissionDenied", value: result.value });
738
+ sendShellStreamEvent(h2Request, execMsg, {
739
+ case: "exit",
740
+ value: create(ShellStreamExitSchema, {
741
+ code: 1,
742
+ cwd: result.value.workingDirectory,
743
+ aborted: false,
744
+ }),
745
+ });
746
+ return;
747
+ }
748
+ default:
749
+ return;
750
+ }
751
+ }
752
+
753
+ async function handleExecServerMessage(
754
+ execMsg: ExecServerMessage,
755
+ h2Request: http2.ClientHttp2Stream,
756
+ execHandlers: CursorExecHandlers | undefined,
757
+ onToolResult: CursorToolResultHandler | undefined,
758
+ requestContextTools: McpToolDefinition[],
759
+ ): Promise<void> {
760
+ const execCase = execMsg.message.case;
761
+ if (execCase === "requestContextArgs") {
762
+ const requestContext = create(RequestContextSchema, {
763
+ rules: [],
764
+ repositoryInfo: [],
765
+ tools: requestContextTools,
766
+ gitRepos: [],
767
+ projectLayouts: [],
768
+ mcpInstructions: [],
769
+ fileContents: {},
770
+ customSubagents: [],
771
+ });
772
+
773
+ const requestContextResult = create(RequestContextResultSchema, {
774
+ result: {
775
+ case: "success",
776
+ value: create(RequestContextSuccessSchema, { requestContext }),
777
+ },
778
+ });
779
+
780
+ sendExecClientMessage(h2Request, execMsg, "requestContextResult", requestContextResult);
781
+ log("execClient", "requestContextResult");
782
+ return;
783
+ }
784
+
785
+ if (!execCase) {
786
+ return;
787
+ }
788
+
789
+ switch (execCase) {
790
+ case "readArgs": {
791
+ const args = execMsg.message.value;
792
+ const { execResult } = await resolveExecHandler(
793
+ args,
794
+ execHandlers?.read,
795
+ onToolResult,
796
+ (toolResult) => buildReadResultFromToolResult(args.path, toolResult),
797
+ (reason) => buildReadRejectedResult(args.path, reason),
798
+ (error) => buildReadErrorResult(args.path, error),
799
+ );
800
+ sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
801
+ return;
802
+ }
803
+ case "lsArgs": {
804
+ const args = execMsg.message.value;
805
+ const { execResult } = await resolveExecHandler(
806
+ args,
807
+ execHandlers?.ls,
808
+ onToolResult,
809
+ (toolResult) => buildLsResultFromToolResult(args.path, toolResult),
810
+ (reason) => buildLsRejectedResult(args.path, reason),
811
+ (error) => buildLsErrorResult(args.path, error),
812
+ );
813
+ sendExecClientMessage(h2Request, execMsg, "lsResult", execResult);
814
+ return;
815
+ }
816
+ case "grepArgs": {
817
+ const args = execMsg.message.value;
818
+ const { execResult } = await resolveExecHandler(
819
+ args,
820
+ execHandlers?.grep,
821
+ onToolResult,
822
+ (toolResult) => buildGrepResultFromToolResult(args, toolResult),
823
+ (reason) => buildGrepErrorResult(reason),
824
+ (error) => buildGrepErrorResult(error),
825
+ );
826
+ sendExecClientMessage(h2Request, execMsg, "grepResult", execResult);
827
+ return;
828
+ }
829
+ case "writeArgs": {
830
+ const args = execMsg.message.value;
831
+ const { execResult } = await resolveExecHandler(
832
+ args,
833
+ execHandlers?.write,
834
+ onToolResult,
835
+ (toolResult) =>
836
+ buildWriteResultFromToolResult(
837
+ {
838
+ path: args.path,
839
+ fileText: args.fileText,
840
+ fileBytes: args.fileBytes,
841
+ returnFileContentAfterWrite: args.returnFileContentAfterWrite,
842
+ },
843
+ toolResult,
844
+ ),
845
+ (reason) => buildWriteRejectedResult(args.path, reason),
846
+ (error) => buildWriteErrorResult(args.path, error),
847
+ );
848
+ sendExecClientMessage(h2Request, execMsg, "writeResult", execResult);
849
+ return;
850
+ }
851
+ case "deleteArgs": {
852
+ const args = execMsg.message.value;
853
+ const { execResult } = await resolveExecHandler(
854
+ args,
855
+ execHandlers?.delete,
856
+ onToolResult,
857
+ (toolResult) => buildDeleteResultFromToolResult(args.path, toolResult),
858
+ (reason) => buildDeleteRejectedResult(args.path, reason),
859
+ (error) => buildDeleteErrorResult(args.path, error),
860
+ );
861
+ sendExecClientMessage(h2Request, execMsg, "deleteResult", execResult);
862
+ return;
863
+ }
864
+ case "shellArgs": {
865
+ const args = execMsg.message.value;
866
+ const { execResult } = await resolveExecHandler(
867
+ args,
868
+ execHandlers?.shell,
869
+ onToolResult,
870
+ (toolResult) => buildShellResultFromToolResult(args, toolResult),
871
+ (reason) => buildShellRejectedResult(args.command, args.workingDirectory, reason),
872
+ (error) => buildShellFailureResult(args.command, args.workingDirectory, error),
873
+ );
874
+ sendExecClientMessage(h2Request, execMsg, "shellResult", execResult);
875
+ return;
876
+ }
877
+ case "shellStreamArgs": {
878
+ const args = execMsg.message.value;
879
+ await handleShellStreamArgs(args, execMsg, h2Request, execHandlers, onToolResult);
880
+ return;
881
+ }
882
+ case "backgroundShellSpawnArgs": {
883
+ const args = execMsg.message.value;
884
+ const execResult = create(BackgroundShellSpawnResultSchema, {
885
+ result: {
886
+ case: "rejected",
887
+ value: create(ShellRejectedSchema, {
888
+ command: args.command,
889
+ workingDirectory: args.workingDirectory,
890
+ reason: "Not implemented",
891
+ isReadonly: false,
892
+ }),
893
+ },
894
+ });
895
+ sendExecClientMessage(h2Request, execMsg, "backgroundShellSpawnResult", execResult);
896
+ return;
897
+ }
898
+ case "writeShellStdinArgs": {
899
+ const execResult = create(WriteShellStdinResultSchema, {
900
+ result: {
901
+ case: "error",
902
+ value: create(WriteShellStdinErrorSchema, {
903
+ error: "Not implemented",
904
+ }),
905
+ },
906
+ });
907
+ sendExecClientMessage(h2Request, execMsg, "writeShellStdinResult", execResult);
908
+ return;
909
+ }
910
+ case "fetchArgs": {
911
+ const args = execMsg.message.value;
912
+ const execResult = create(FetchResultSchema, {
913
+ result: {
914
+ case: "error",
915
+ value: create(FetchErrorSchema, {
916
+ url: args.url,
917
+ error: "Not implemented",
918
+ }),
919
+ },
920
+ });
921
+ sendExecClientMessage(h2Request, execMsg, "fetchResult", execResult);
922
+ return;
923
+ }
924
+ case "diagnosticsArgs": {
925
+ const args = execMsg.message.value;
926
+ const { execResult } = await resolveExecHandler(
927
+ args,
928
+ execHandlers?.diagnostics,
929
+ onToolResult,
930
+ (toolResult) => buildDiagnosticsResultFromToolResult(args.path, toolResult),
931
+ (reason) => buildDiagnosticsRejectedResult(args.path, reason),
932
+ (error) => buildDiagnosticsErrorResult(args.path, error),
933
+ );
934
+ sendExecClientMessage(h2Request, execMsg, "diagnosticsResult", execResult);
935
+ return;
936
+ }
937
+ case "mcpArgs": {
938
+ const args = execMsg.message.value;
939
+ const mcpCall = decodeMcpCall(args);
940
+ const { execResult } = await resolveExecHandler(
941
+ mcpCall,
942
+ execHandlers?.mcp,
943
+ onToolResult,
944
+ (toolResult) => buildMcpResultFromToolResult(mcpCall, toolResult),
945
+ (_reason) => buildMcpToolNotFoundResult(mcpCall),
946
+ (error) => buildMcpErrorResult(error),
947
+ );
948
+ sendExecClientMessage(h2Request, execMsg, "mcpResult", execResult);
949
+ return;
950
+ }
951
+ default:
952
+ log("warn", "unhandledExecMessage", { execCase });
953
+ }
954
+ }
955
+
956
+ function sendExecClientMessage<T>(
957
+ h2Request: http2.ClientHttp2Stream,
958
+ execMsg: ExecServerMessage,
959
+ messageCase: ExecClientMessage["message"]["case"],
960
+ value: T,
961
+ ): void {
962
+ const execClientMessage = create(ExecClientMessageSchema, {
963
+ id: execMsg.id,
964
+ execId: execMsg.execId,
965
+ message: {
966
+ case: messageCase,
967
+ value: value as any,
968
+ },
969
+ });
970
+
971
+ const clientMessage = create(AgentClientMessageSchema, {
972
+ message: { case: "execClientMessage", value: execClientMessage },
973
+ });
974
+
975
+ const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
976
+ h2Request.write(frameConnectMessage(responseBytes));
977
+
978
+ log("execClientMessage", messageCase, value);
979
+ }
980
+
981
+ async function resolveExecHandler<TArgs, TResult>(
982
+ args: TArgs,
983
+ handler: ((args: TArgs) => Promise<CursorExecHandlerResult<TResult>>) | undefined,
984
+ onToolResult: CursorToolResultHandler | undefined,
985
+ buildFromToolResult: (toolResult: ToolResultMessage) => TResult,
986
+ buildRejected: (reason: string) => TResult,
987
+ buildError: (error: string) => TResult,
988
+ ): Promise<{ execResult: TResult; toolResult?: ToolResultMessage }> {
989
+ if (!handler) {
990
+ return { execResult: buildRejected("Tool not available") };
991
+ }
992
+
993
+ try {
994
+ const handlerResult = await handler(args);
995
+ const { execResult, toolResult } = splitExecHandlerResult(handlerResult);
996
+ const finalToolResult = await applyToolResultHandler(toolResult, onToolResult);
997
+
998
+ if (execResult) {
999
+ return { execResult, toolResult: finalToolResult };
1000
+ }
1001
+ if (finalToolResult) {
1002
+ return { execResult: buildFromToolResult(finalToolResult), toolResult: finalToolResult };
1003
+ }
1004
+ return { execResult: buildRejected("Tool returned no result") };
1005
+ } catch (error) {
1006
+ const message = error instanceof Error ? error.message : String(error);
1007
+ return { execResult: buildError(message) };
1008
+ }
1009
+ }
1010
+
1011
+ function splitExecHandlerResult<TResult>(result: CursorExecHandlerResult<TResult>): {
1012
+ execResult?: TResult;
1013
+ toolResult?: ToolResultMessage;
1014
+ } {
1015
+ if (isToolResultMessage(result)) {
1016
+ return { toolResult: result };
1017
+ }
1018
+ if (result && typeof result === "object") {
1019
+ const record = result as Record<string, unknown>;
1020
+ if ("execResult" in record) {
1021
+ const { execResult, toolResult } = record as {
1022
+ execResult: TResult;
1023
+ toolResult?: ToolResultMessage;
1024
+ };
1025
+ return { execResult, toolResult };
1026
+ }
1027
+ if ("toolResult" in record && !isToolResultMessage(record)) {
1028
+ const { result: execResult, toolResult } = record as {
1029
+ result?: TResult;
1030
+ toolResult?: ToolResultMessage;
1031
+ };
1032
+ return { execResult, toolResult };
1033
+ }
1034
+ if ("result" in record && !("$typeName" in record)) {
1035
+ const { result: execResult, toolResult } = record as {
1036
+ result: TResult;
1037
+ toolResult?: ToolResultMessage;
1038
+ };
1039
+ return { execResult, toolResult };
1040
+ }
1041
+ }
1042
+ return { execResult: result as TResult };
1043
+ }
1044
+
1045
+ function isToolResultMessage(value: unknown): value is ToolResultMessage {
1046
+ return !!value && typeof value === "object" && (value as ToolResultMessage).role === "toolResult";
1047
+ }
1048
+
1049
+ async function applyToolResultHandler(
1050
+ toolResult: ToolResultMessage | undefined,
1051
+ onToolResult: CursorToolResultHandler | undefined,
1052
+ ): Promise<ToolResultMessage | undefined> {
1053
+ if (!toolResult || !onToolResult) {
1054
+ return toolResult;
1055
+ }
1056
+ const updated = await onToolResult(toolResult);
1057
+ return updated ?? toolResult;
1058
+ }
1059
+
1060
+ function toolResultToText(toolResult: ToolResultMessage): string {
1061
+ return toolResult.content.map((item) => (item.type === "text" ? item.text : `[${item.mimeType} image]`)).join("\n");
1062
+ }
1063
+
1064
+ function toolResultWasTruncated(toolResult: ToolResultMessage): boolean {
1065
+ if (!toolResult.details || typeof toolResult.details !== "object") {
1066
+ return false;
1067
+ }
1068
+ const truncation = (toolResult.details as { truncation?: { truncated?: boolean } }).truncation;
1069
+ return !!truncation?.truncated;
1070
+ }
1071
+
1072
+ function toolResultDetailBoolean(toolResult: ToolResultMessage, key: string): boolean {
1073
+ if (!toolResult.details || typeof toolResult.details !== "object") {
1074
+ return false;
1075
+ }
1076
+ const value = (toolResult.details as Record<string, unknown>)[key];
1077
+ return typeof value === "boolean" ? value : false;
1078
+ }
1079
+
1080
+ function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1081
+ const text = toolResultToText(toolResult);
1082
+ if (toolResult.isError) {
1083
+ return buildReadErrorResult(path, text || "Read failed");
1084
+ }
1085
+ const totalLines = text ? text.split("\n").length : 0;
1086
+ return create(ReadResultSchema, {
1087
+ result: {
1088
+ case: "success",
1089
+ value: create(ReadSuccessSchema, {
1090
+ path,
1091
+ totalLines,
1092
+ fileSize: BigInt(Buffer.byteLength(text, "utf-8")),
1093
+ truncated: toolResultWasTruncated(toolResult),
1094
+ output: { case: "content", value: text },
1095
+ }),
1096
+ },
1097
+ });
1098
+ }
1099
+
1100
+ function buildReadErrorResult(path: string, error: string) {
1101
+ return create(ReadResultSchema, {
1102
+ result: {
1103
+ case: "error",
1104
+ value: create(ReadErrorSchema, { path, error }),
1105
+ },
1106
+ });
1107
+ }
1108
+
1109
+ function buildReadRejectedResult(path: string, reason: string) {
1110
+ return create(ReadResultSchema, {
1111
+ result: {
1112
+ case: "rejected",
1113
+ value: create(ReadRejectedSchema, { path, reason }),
1114
+ },
1115
+ });
1116
+ }
1117
+
1118
+ function buildWriteResultFromToolResult(
1119
+ args: { path: string; fileText?: string; fileBytes?: Uint8Array; returnFileContentAfterWrite?: boolean },
1120
+ toolResult: ToolResultMessage,
1121
+ ) {
1122
+ const text = toolResultToText(toolResult);
1123
+ if (toolResult.isError) {
1124
+ return buildWriteErrorResult(args.path, text || "Write failed");
1125
+ }
1126
+ const fileText = args.fileText ?? "";
1127
+ const fileSize = args.fileBytes?.length ?? Buffer.byteLength(fileText, "utf-8");
1128
+ const linesCreated = fileText ? fileText.split("\n").length : 0;
1129
+ return create(WriteResultSchema, {
1130
+ result: {
1131
+ case: "success",
1132
+ value: create(WriteSuccessSchema, {
1133
+ path: args.path,
1134
+ linesCreated,
1135
+ fileSize,
1136
+ fileContentAfterWrite: args.returnFileContentAfterWrite ? fileText : undefined,
1137
+ }),
1138
+ },
1139
+ });
1140
+ }
1141
+
1142
+ function buildWriteErrorResult(path: string, error: string) {
1143
+ return create(WriteResultSchema, {
1144
+ result: {
1145
+ case: "error",
1146
+ value: create(WriteErrorSchema, { path, error }),
1147
+ },
1148
+ });
1149
+ }
1150
+
1151
+ function buildWriteRejectedResult(path: string, reason: string) {
1152
+ return create(WriteResultSchema, {
1153
+ result: {
1154
+ case: "rejected",
1155
+ value: create(WriteRejectedSchema, { path, reason }),
1156
+ },
1157
+ });
1158
+ }
1159
+
1160
+ function buildDeleteResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1161
+ const text = toolResultToText(toolResult);
1162
+ if (toolResult.isError) {
1163
+ return buildDeleteErrorResult(path, text || "Delete failed");
1164
+ }
1165
+ return create(DeleteResultSchema, {
1166
+ result: {
1167
+ case: "success",
1168
+ value: create(DeleteSuccessSchema, {
1169
+ path,
1170
+ deletedFile: path,
1171
+ fileSize: BigInt(0),
1172
+ prevContent: "",
1173
+ }),
1174
+ },
1175
+ });
1176
+ }
1177
+
1178
+ function buildDeleteErrorResult(path: string, error: string) {
1179
+ return create(DeleteResultSchema, {
1180
+ result: {
1181
+ case: "error",
1182
+ value: create(DeleteErrorSchema, { path, error }),
1183
+ },
1184
+ });
1185
+ }
1186
+
1187
+ function buildDeleteRejectedResult(path: string, reason: string) {
1188
+ return create(DeleteResultSchema, {
1189
+ result: {
1190
+ case: "rejected",
1191
+ value: create(DeleteRejectedSchema, { path, reason }),
1192
+ },
1193
+ });
1194
+ }
1195
+
1196
+ function buildShellResultFromToolResult(
1197
+ args: { command: string; workingDirectory: string },
1198
+ toolResult: ToolResultMessage,
1199
+ ) {
1200
+ const output = toolResultToText(toolResult);
1201
+ if (toolResult.isError) {
1202
+ return buildShellFailureResult(args.command, args.workingDirectory, output || "Shell failed");
1203
+ }
1204
+ return create(ShellResultSchema, {
1205
+ result: {
1206
+ case: "success",
1207
+ value: create(ShellSuccessSchema, {
1208
+ command: args.command,
1209
+ workingDirectory: args.workingDirectory,
1210
+ exitCode: 0,
1211
+ signal: "",
1212
+ stdout: output,
1213
+ stderr: "",
1214
+ executionTime: 0,
1215
+ }),
1216
+ },
1217
+ });
1218
+ }
1219
+
1220
+ function buildShellFailureResult(command: string, workingDirectory: string, error: string) {
1221
+ return create(ShellResultSchema, {
1222
+ result: {
1223
+ case: "failure",
1224
+ value: create(ShellFailureSchema, {
1225
+ command,
1226
+ workingDirectory,
1227
+ exitCode: 1,
1228
+ signal: "",
1229
+ stdout: "",
1230
+ stderr: error,
1231
+ executionTime: 0,
1232
+ aborted: false,
1233
+ }),
1234
+ },
1235
+ });
1236
+ }
1237
+
1238
+ function buildShellRejectedResult(command: string, workingDirectory: string, reason: string) {
1239
+ return create(ShellResultSchema, {
1240
+ result: {
1241
+ case: "rejected",
1242
+ value: create(ShellRejectedSchema, {
1243
+ command,
1244
+ workingDirectory,
1245
+ reason,
1246
+ isReadonly: false,
1247
+ }),
1248
+ },
1249
+ });
1250
+ }
1251
+
1252
+ function buildLsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1253
+ const text = toolResultToText(toolResult);
1254
+ if (toolResult.isError) {
1255
+ return buildLsErrorResult(path, text || "Ls failed");
1256
+ }
1257
+ const rootPath = path || ".";
1258
+ const entries = text
1259
+ .split("\n")
1260
+ .map((line) => line.trim())
1261
+ .filter((line) => line.length > 0 && !line.startsWith("["));
1262
+ const childrenDirs: LsDirectoryTreeNode[] = [];
1263
+ const childrenFiles: LsDirectoryTreeNode_File[] = [];
1264
+
1265
+ for (const entry of entries) {
1266
+ const name = entry.split(" (")[0];
1267
+ if (name.endsWith("/")) {
1268
+ const dirName = name.slice(0, -1);
1269
+ childrenDirs.push(
1270
+ create(LsDirectoryTreeNodeSchema, {
1271
+ absPath: `${rootPath.replace(/\/$/, "")}/${dirName}`,
1272
+ childrenDirs: [],
1273
+ childrenFiles: [],
1274
+ childrenWereProcessed: false,
1275
+ fullSubtreeExtensionCounts: {},
1276
+ numFiles: 0,
1277
+ }),
1278
+ );
1279
+ } else {
1280
+ childrenFiles.push(create(LsDirectoryTreeNode_FileSchema, { name }));
1281
+ }
1282
+ }
1283
+
1284
+ const root = create(LsDirectoryTreeNodeSchema, {
1285
+ absPath: rootPath,
1286
+ childrenDirs,
1287
+ childrenFiles,
1288
+ childrenWereProcessed: true,
1289
+ fullSubtreeExtensionCounts: {},
1290
+ numFiles: childrenFiles.length,
1291
+ });
1292
+
1293
+ return create(LsResultSchema, {
1294
+ result: {
1295
+ case: "success",
1296
+ value: create(LsSuccessSchema, { directoryTreeRoot: root }),
1297
+ },
1298
+ });
1299
+ }
1300
+
1301
+ function buildLsErrorResult(path: string, error: string) {
1302
+ return create(LsResultSchema, {
1303
+ result: {
1304
+ case: "error",
1305
+ value: create(LsErrorSchema, { path, error }),
1306
+ },
1307
+ });
1308
+ }
1309
+
1310
+ function buildLsRejectedResult(path: string, reason: string) {
1311
+ return create(LsResultSchema, {
1312
+ result: {
1313
+ case: "rejected",
1314
+ value: create(LsRejectedSchema, { path, reason }),
1315
+ },
1316
+ });
1317
+ }
1318
+
1319
+ function buildGrepResultFromToolResult(
1320
+ args: { pattern: string; path?: string; outputMode?: string },
1321
+ toolResult: ToolResultMessage,
1322
+ ) {
1323
+ const text = toolResultToText(toolResult);
1324
+ if (toolResult.isError) {
1325
+ return buildGrepErrorResult(text || "Grep failed");
1326
+ }
1327
+
1328
+ const outputMode = args.outputMode || "content";
1329
+ const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
1330
+ const lines = text
1331
+ .split("\n")
1332
+ .map((line) => line.trimEnd())
1333
+ .filter((line) => line.length > 0 && !line.startsWith("[") && !line.toLowerCase().startsWith("no matches"));
1334
+
1335
+ const workspaceKey = args.path || ".";
1336
+ let unionResult: GrepUnionResult;
1337
+
1338
+ if (outputMode === "files_with_matches") {
1339
+ const files = lines;
1340
+ unionResult = create(GrepUnionResultSchema, {
1341
+ result: {
1342
+ case: "files",
1343
+ value: create(GrepFilesResultSchema, {
1344
+ files,
1345
+ totalFiles: files.length,
1346
+ clientTruncated,
1347
+ ripgrepTruncated: false,
1348
+ }),
1349
+ },
1350
+ });
1351
+ } else if (outputMode === "count") {
1352
+ const counts = lines
1353
+ .map((line) => {
1354
+ const separatorIndex = line.lastIndexOf(":");
1355
+ if (separatorIndex === -1) {
1356
+ return null;
1357
+ }
1358
+ const file = line.slice(0, separatorIndex);
1359
+ const count = Number.parseInt(line.slice(separatorIndex + 1), 10);
1360
+ if (!file || Number.isNaN(count)) {
1361
+ return null;
1362
+ }
1363
+ return create(GrepFileCountSchema, { file, count });
1364
+ })
1365
+ .filter((entry): entry is GrepFileCount => entry !== null);
1366
+ const totalMatches = counts.reduce((sum, entry) => sum + entry.count, 0);
1367
+ unionResult = create(GrepUnionResultSchema, {
1368
+ result: {
1369
+ case: "count",
1370
+ value: create(GrepCountResultSchema, {
1371
+ counts,
1372
+ totalFiles: counts.length,
1373
+ totalMatches,
1374
+ clientTruncated,
1375
+ ripgrepTruncated: false,
1376
+ }),
1377
+ },
1378
+ });
1379
+ } else {
1380
+ const matchMap = new Map<string, Array<{ line: number; content: string; isContextLine: boolean }>>();
1381
+ let totalMatchedLines = 0;
1382
+
1383
+ for (const line of lines) {
1384
+ const matchLine = line.match(/^(.+?):(\d+):\s?(.*)$/);
1385
+ const contextLine = line.match(/^(.+?)-(\d+)-\s?(.*)$/);
1386
+ const match = matchLine ?? contextLine;
1387
+ if (!match) {
1388
+ continue;
1389
+ }
1390
+ const [, file, lineNumber, content] = match;
1391
+ const isContextLine = Boolean(contextLine);
1392
+ const list = matchMap.get(file) ?? [];
1393
+ list.push({ line: Number(lineNumber), content, isContextLine });
1394
+ matchMap.set(file, list);
1395
+ if (!isContextLine) {
1396
+ totalMatchedLines += 1;
1397
+ }
1398
+ }
1399
+
1400
+ const matches = Array.from(matchMap.entries()).map(([file, matches]) =>
1401
+ create(GrepFileMatchSchema, {
1402
+ file,
1403
+ matches: matches.map((entry) =>
1404
+ create(GrepContentMatchSchema, {
1405
+ lineNumber: entry.line,
1406
+ content: entry.content,
1407
+ contentTruncated: false,
1408
+ isContextLine: entry.isContextLine,
1409
+ }),
1410
+ ),
1411
+ }),
1412
+ );
1413
+ const totalLines = matches.reduce((sum, entry) => sum + entry.matches.length, 0);
1414
+ unionResult = create(GrepUnionResultSchema, {
1415
+ result: {
1416
+ case: "content",
1417
+ value: create(GrepContentResultSchema, {
1418
+ matches,
1419
+ totalLines,
1420
+ totalMatchedLines,
1421
+ clientTruncated,
1422
+ ripgrepTruncated: false,
1423
+ }),
1424
+ },
1425
+ });
1426
+ }
1427
+
1428
+ return create(GrepResultSchema, {
1429
+ result: {
1430
+ case: "success",
1431
+ value: create(GrepSuccessSchema, {
1432
+ pattern: args.pattern,
1433
+ path: args.path || "",
1434
+ outputMode,
1435
+ workspaceResults: { [workspaceKey]: unionResult },
1436
+ }),
1437
+ },
1438
+ });
1439
+ }
1440
+
1441
+ function buildGrepErrorResult(error: string) {
1442
+ return create(GrepResultSchema, {
1443
+ result: {
1444
+ case: "error",
1445
+ value: create(GrepErrorSchema, { error }),
1446
+ },
1447
+ });
1448
+ }
1449
+
1450
+ function buildDiagnosticsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1451
+ const text = toolResultToText(toolResult);
1452
+ if (toolResult.isError) {
1453
+ return buildDiagnosticsErrorResult(path, text || "Diagnostics failed");
1454
+ }
1455
+ return create(DiagnosticsResultSchema, {
1456
+ result: {
1457
+ case: "success",
1458
+ value: create(DiagnosticsSuccessSchema, {
1459
+ path,
1460
+ diagnostics: [],
1461
+ totalDiagnostics: 0,
1462
+ }),
1463
+ },
1464
+ });
1465
+ }
1466
+
1467
+ function buildDiagnosticsErrorResult(_path: string, error: string) {
1468
+ return create(DiagnosticsResultSchema, {
1469
+ result: {
1470
+ case: "error",
1471
+ value: create(DiagnosticsErrorSchema, { error }),
1472
+ },
1473
+ });
1474
+ }
1475
+
1476
+ function buildDiagnosticsRejectedResult(path: string, reason: string) {
1477
+ return create(DiagnosticsResultSchema, {
1478
+ result: {
1479
+ case: "rejected",
1480
+ value: create(DiagnosticsRejectedSchema, { path, reason }),
1481
+ },
1482
+ });
1483
+ }
1484
+
1485
+ function parseToolArgsJson(text: string): unknown {
1486
+ const trimmed = text.trim();
1487
+ if (!trimmed) {
1488
+ return text;
1489
+ }
1490
+ try {
1491
+ const normalized = trimmed
1492
+ .replace(/\bNone\b/g, "null")
1493
+ .replace(/\bTrue\b/g, "true")
1494
+ .replace(/\bFalse\b/g, "false");
1495
+ return JSON5.parse(normalized);
1496
+ } catch {}
1497
+ return text;
1498
+ }
1499
+
1500
+ function decodeMcpArgValue(value: Uint8Array): unknown {
1501
+ try {
1502
+ const parsedValue = fromBinary(ValueSchema, value);
1503
+ const jsonValue = toJson(ValueSchema, parsedValue) as JsonValue;
1504
+ if (typeof jsonValue === "string") {
1505
+ return parseToolArgsJson(jsonValue);
1506
+ }
1507
+ return jsonValue;
1508
+ } catch {}
1509
+ const text = new TextDecoder().decode(value);
1510
+ return parseToolArgsJson(text);
1511
+ }
1512
+
1513
+ function decodeMcpArgsMap(args?: Record<string, Uint8Array>): Record<string, unknown> | undefined {
1514
+ if (!args) {
1515
+ return undefined;
1516
+ }
1517
+ const decoded: Record<string, unknown> = {};
1518
+ for (const [key, value] of Object.entries(args)) {
1519
+ decoded[key] = decodeMcpArgValue(value);
1520
+ }
1521
+ return decoded;
1522
+ }
1523
+
1524
+ function decodeMcpCall(args: {
1525
+ name: string;
1526
+ args: Record<string, Uint8Array>;
1527
+ toolCallId: string;
1528
+ providerIdentifier: string;
1529
+ toolName: string;
1530
+ }): CursorMcpCall {
1531
+ const decodedArgs: Record<string, unknown> = {};
1532
+ for (const [key, value] of Object.entries(args.args ?? {})) {
1533
+ decodedArgs[key] = decodeMcpArgValue(value);
1534
+ }
1535
+ return {
1536
+ name: args.name,
1537
+ providerIdentifier: args.providerIdentifier,
1538
+ toolName: args.toolName || args.name,
1539
+ toolCallId: args.toolCallId,
1540
+ args: decodedArgs,
1541
+ rawArgs: args.args ?? {},
1542
+ };
1543
+ }
1544
+
1545
+ function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) {
1546
+ if (toolResult.isError) {
1547
+ return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
1548
+ }
1549
+ const content = toolResult.content.map((item) => {
1550
+ if (item.type === "image") {
1551
+ return create(McpToolResultContentItemSchema, {
1552
+ content: {
1553
+ case: "image",
1554
+ value: create(McpImageContentSchema, {
1555
+ data: Uint8Array.from(Buffer.from(item.data, "base64")),
1556
+ mimeType: item.mimeType,
1557
+ }),
1558
+ },
1559
+ });
1560
+ }
1561
+ return create(McpToolResultContentItemSchema, {
1562
+ content: {
1563
+ case: "text",
1564
+ value: create(McpTextContentSchema, { text: item.text }),
1565
+ },
1566
+ });
1567
+ });
1568
+
1569
+ return create(McpResultSchema, {
1570
+ result: {
1571
+ case: "success",
1572
+ value: create(McpSuccessSchema, {
1573
+ content,
1574
+ isError: false,
1575
+ }),
1576
+ },
1577
+ });
1578
+ }
1579
+
1580
+ function buildMcpToolNotFoundResult(mcpCall: CursorMcpCall) {
1581
+ return create(McpResultSchema, {
1582
+ result: {
1583
+ case: "toolNotFound",
1584
+ value: create(McpToolNotFoundSchema, { name: mcpCall.toolName, availableTools: [] }),
1585
+ },
1586
+ });
1587
+ }
1588
+
1589
+ function buildMcpErrorResult(error: string) {
1590
+ return create(McpResultSchema, {
1591
+ result: {
1592
+ case: "error",
1593
+ value: create(McpErrorSchema, { error }),
1594
+ },
1595
+ });
1596
+ }
1597
+
1598
+ function processInteractionUpdate(
1599
+ update: any,
1600
+ output: AssistantMessage,
1601
+ stream: AssistantMessageEventStream,
1602
+ state: BlockState,
1603
+ usageState: UsageState,
1604
+ ): void {
1605
+ const updateCase = update.message?.case;
1606
+
1607
+ log("interactionUpdate", updateCase, update.message?.value);
1608
+
1609
+ if (updateCase === "textDelta") {
1610
+ const delta = update.message.value.text || "";
1611
+ if (!state.currentTextBlock) {
1612
+ const block: TextContent & { index: number } = {
1613
+ type: "text",
1614
+ text: "",
1615
+ index: output.content.length,
1616
+ };
1617
+ output.content.push(block);
1618
+ state.setTextBlock(block);
1619
+ stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
1620
+ }
1621
+ state.currentTextBlock!.text += delta;
1622
+ const idx = output.content.indexOf(state.currentTextBlock!);
1623
+ stream.push({ type: "text_delta", contentIndex: idx, delta, partial: output });
1624
+ } else if (updateCase === "thinkingDelta") {
1625
+ const delta = update.message.value.text || "";
1626
+ if (!state.currentThinkingBlock) {
1627
+ const block: ThinkingContent & { index: number } = {
1628
+ type: "thinking",
1629
+ thinking: "",
1630
+ index: output.content.length,
1631
+ };
1632
+ output.content.push(block);
1633
+ state.setThinkingBlock(block);
1634
+ stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
1635
+ }
1636
+ state.currentThinkingBlock!.thinking += delta;
1637
+ const idx = output.content.indexOf(state.currentThinkingBlock!);
1638
+ stream.push({ type: "thinking_delta", contentIndex: idx, delta, partial: output });
1639
+ } else if (updateCase === "thinkingCompleted") {
1640
+ if (state.currentThinkingBlock) {
1641
+ const idx = output.content.indexOf(state.currentThinkingBlock);
1642
+ delete (state.currentThinkingBlock as any).index;
1643
+ stream.push({
1644
+ type: "thinking_end",
1645
+ contentIndex: idx,
1646
+ content: state.currentThinkingBlock.thinking,
1647
+ partial: output,
1648
+ });
1649
+ state.setThinkingBlock(null);
1650
+ }
1651
+ } else if (updateCase === "toolCallStarted") {
1652
+ const toolCall = update.message.value.toolCall;
1653
+ if (toolCall) {
1654
+ const mcpCall = toolCall.mcpToolCall;
1655
+ if (mcpCall) {
1656
+ const args = mcpCall.args || {};
1657
+ const block: ToolCall & { index: number; partialJson: string } = {
1658
+ type: "toolCall",
1659
+ id: args.toolCallId || crypto.randomUUID(),
1660
+ name: args.name || args.toolName || "",
1661
+ arguments: {},
1662
+ index: output.content.length,
1663
+ partialJson: "",
1664
+ };
1665
+ output.content.push(block);
1666
+ state.setToolCall(block);
1667
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
1668
+ }
1669
+ }
1670
+ } else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
1671
+ if (state.currentToolCall) {
1672
+ const delta = update.message.value.argsTextDelta || "";
1673
+ state.currentToolCall.partialJson += delta;
1674
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
1675
+ const idx = output.content.indexOf(state.currentToolCall);
1676
+ stream.push({ type: "toolcall_delta", contentIndex: idx, delta, partial: output });
1677
+ }
1678
+ } else if (updateCase === "toolCallCompleted") {
1679
+ if (state.currentToolCall) {
1680
+ const toolCall = update.message.value.toolCall;
1681
+ const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args);
1682
+ if (decodedArgs) {
1683
+ state.currentToolCall.arguments = decodedArgs;
1684
+ }
1685
+ const idx = output.content.indexOf(state.currentToolCall);
1686
+ delete (state.currentToolCall as any).partialJson;
1687
+ delete (state.currentToolCall as any).index;
1688
+ stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: state.currentToolCall, partial: output });
1689
+ state.setToolCall(null);
1690
+ }
1691
+ } else if (updateCase === "turnEnded") {
1692
+ output.stopReason = "stop";
1693
+ } else if (updateCase === "tokenDelta") {
1694
+ const tokenDelta = update.message.value;
1695
+ usageState.sawTokenDelta = true;
1696
+ output.usage.output += tokenDelta.tokens || 0;
1697
+ output.usage.totalTokens = output.usage.input + output.usage.output;
1698
+ }
1699
+ }
1700
+
1701
+ function handleConversationCheckpointUpdate(
1702
+ checkpoint: ConversationStateStructure,
1703
+ output: AssistantMessage,
1704
+ usageState: UsageState,
1705
+ onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
1706
+ ): void {
1707
+ onConversationCheckpoint?.(checkpoint);
1708
+ if (usageState.sawTokenDelta) {
1709
+ return;
1710
+ }
1711
+ const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
1712
+ if (usedTokens <= 0) {
1713
+ return;
1714
+ }
1715
+ if (output.usage.output !== usedTokens) {
1716
+ output.usage.output = usedTokens;
1717
+ output.usage.totalTokens = output.usage.input + output.usage.output;
1718
+ }
1719
+ }
1720
+
1721
+ function createBlobId(data: Uint8Array): Uint8Array {
1722
+ return new Uint8Array(createHash("sha256").update(data).digest());
1723
+ }
1724
+
1725
+ const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp"]);
1726
+
1727
+ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
1728
+ if (!tools || tools.length === 0) {
1729
+ return [];
1730
+ }
1731
+
1732
+ const advertisedTools = tools.filter((tool) => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name));
1733
+ if (advertisedTools.length === 0) {
1734
+ return [];
1735
+ }
1736
+
1737
+ return advertisedTools.map((tool) => {
1738
+ const jsonSchema = tool.parameters as Record<string, unknown> | undefined;
1739
+ const schemaValue: JsonValue =
1740
+ jsonSchema && typeof jsonSchema === "object"
1741
+ ? (jsonSchema as JsonValue)
1742
+ : { type: "object", properties: {}, required: [] };
1743
+ const inputSchema = toBinary(ValueSchema, fromJson(ValueSchema, schemaValue));
1744
+ return create(McpToolDefinitionSchema, {
1745
+ name: tool.name,
1746
+ description: tool.description,
1747
+ providerIdentifier: "pi-agent",
1748
+ toolName: tool.name,
1749
+ inputSchema,
1750
+ });
1751
+ });
1752
+ }
1753
+
1754
+ /**
1755
+ * Extract text content from a user message.
1756
+ */
1757
+ function extractUserMessageText(msg: Message): string {
1758
+ if (msg.role !== "user") return "";
1759
+ const content = msg.content;
1760
+ if (typeof content === "string") return content;
1761
+ return content
1762
+ .filter((c): c is TextContent => c.type === "text")
1763
+ .map((c) => c.text)
1764
+ .join("\n");
1765
+ }
1766
+
1767
+ /**
1768
+ * Extract text content from an assistant message.
1769
+ */
1770
+ function extractAssistantMessageText(msg: Message): string {
1771
+ if (msg.role !== "assistant") return "";
1772
+ if (!Array.isArray(msg.content)) return "";
1773
+ return msg.content
1774
+ .filter((c): c is TextContent => c.type === "text")
1775
+ .map((c) => c.text)
1776
+ .join("\n");
1777
+ }
1778
+
1779
+ /**
1780
+ * Convert context.messages to Cursor's serialized ConversationTurn format.
1781
+ * Groups messages into turns: each turn is a user message followed by the assistant's response.
1782
+ * Excludes the last user message (which goes in the action).
1783
+ * Returns serialized bytes for ConversationStateStructure.turns field.
1784
+ */
1785
+ function buildConversationTurns(messages: Message[]): Uint8Array[] {
1786
+ const turns: Uint8Array[] = [];
1787
+
1788
+ // Find turn boundaries - each turn starts with a user message
1789
+ let i = 0;
1790
+ while (i < messages.length) {
1791
+ const msg = messages[i];
1792
+
1793
+ // Skip non-user messages at the start
1794
+ if (msg.role !== "user") {
1795
+ i++;
1796
+ continue;
1797
+ }
1798
+
1799
+ // Check if this is the last user message (which goes in the action, not turns)
1800
+ let isLastUserMessage = true;
1801
+ for (let j = i + 1; j < messages.length; j++) {
1802
+ if (messages[j].role === "user") {
1803
+ isLastUserMessage = false;
1804
+ break;
1805
+ }
1806
+ }
1807
+ if (isLastUserMessage) {
1808
+ break;
1809
+ }
1810
+
1811
+ // Create and serialize user message
1812
+ const userText = extractUserMessageText(msg);
1813
+ if (!userText) {
1814
+ i++;
1815
+ continue;
1816
+ }
1817
+
1818
+ const userMessage = create(UserMessageSchema, {
1819
+ text: userText,
1820
+ messageId: crypto.randomUUID(),
1821
+ });
1822
+ const userMessageBytes = toBinary(UserMessageSchema, userMessage);
1823
+
1824
+ // Collect and serialize steps until next user message
1825
+ const stepBytes: Uint8Array[] = [];
1826
+ i++;
1827
+
1828
+ while (i < messages.length && messages[i].role !== "user") {
1829
+ const stepMsg = messages[i];
1830
+
1831
+ if (stepMsg.role === "assistant") {
1832
+ const text = extractAssistantMessageText(stepMsg);
1833
+ if (text) {
1834
+ const step = create(ConversationStepSchema, {
1835
+ message: {
1836
+ case: "assistantMessage",
1837
+ value: create(AssistantMessageSchema, { text }),
1838
+ },
1839
+ });
1840
+ stepBytes.push(toBinary(ConversationStepSchema, step));
1841
+ }
1842
+ } else if (stepMsg.role === "toolResult") {
1843
+ // Include tool results as assistant text for context
1844
+ const text = toolResultToText(stepMsg);
1845
+ if (text) {
1846
+ const step = create(ConversationStepSchema, {
1847
+ message: {
1848
+ case: "assistantMessage",
1849
+ value: create(AssistantMessageSchema, { text: `[Tool Result]\n${text}` }),
1850
+ },
1851
+ });
1852
+ stepBytes.push(toBinary(ConversationStepSchema, step));
1853
+ }
1854
+ }
1855
+
1856
+ i++;
1857
+ }
1858
+
1859
+ // Create the serialized turn using Structure types (bytes)
1860
+ const agentTurn = create(AgentConversationTurnStructureSchema, {
1861
+ userMessage: userMessageBytes,
1862
+ steps: stepBytes,
1863
+ });
1864
+ const turn = create(ConversationTurnStructureSchema, {
1865
+ turn: {
1866
+ case: "agentConversationTurn",
1867
+ value: agentTurn,
1868
+ },
1869
+ });
1870
+ turns.push(toBinary(ConversationTurnStructureSchema, turn));
1871
+ }
1872
+
1873
+ return turns;
1874
+ }
1875
+
1876
+ function buildGrpcRequest(
1877
+ model: Model<"cursor-agent">,
1878
+ context: Context,
1879
+ options: CursorOptions | undefined,
1880
+ state: {
1881
+ conversationId: string;
1882
+ blobStore: Map<string, Uint8Array>;
1883
+ conversationState?: ConversationStateStructure;
1884
+ },
1885
+ ): {
1886
+ requestBytes: Uint8Array;
1887
+ blobStore: Map<string, Uint8Array>;
1888
+ conversationState: ConversationStateStructure;
1889
+ } {
1890
+ const blobStore = state.blobStore;
1891
+
1892
+ const systemPromptJson = JSON.stringify({
1893
+ role: "system",
1894
+ content: context.systemPrompt || "You are a helpful assistant.",
1895
+ });
1896
+ const systemPromptBytes = new TextEncoder().encode(systemPromptJson);
1897
+ const systemPromptId = createBlobId(systemPromptBytes);
1898
+ blobStore.set(Buffer.from(systemPromptId).toString("hex"), systemPromptBytes);
1899
+
1900
+ const lastMessage = context.messages[context.messages.length - 1];
1901
+ const userText =
1902
+ lastMessage?.role === "user"
1903
+ ? typeof lastMessage.content === "string"
1904
+ ? lastMessage.content
1905
+ : extractText(lastMessage.content)
1906
+ : "";
1907
+
1908
+ const userMessage = create(UserMessageSchema, {
1909
+ text: userText,
1910
+ messageId: crypto.randomUUID(),
1911
+ });
1912
+
1913
+ const action = create(ConversationActionSchema, {
1914
+ action: {
1915
+ case: "userMessageAction",
1916
+ value: create(UserMessageActionSchema, { userMessage }),
1917
+ },
1918
+ });
1919
+
1920
+ // Build conversation turns from prior messages (excluding the last user message)
1921
+ const turns = buildConversationTurns(context.messages);
1922
+
1923
+ const hasMatchingPrompt = state.conversationState?.rootPromptMessagesJson?.some((entry) =>
1924
+ Buffer.from(entry).equals(systemPromptId),
1925
+ );
1926
+
1927
+ // Use cached state if available and system prompt matches, but always update turns
1928
+ // from context.messages to ensure full conversation history is sent
1929
+ const baseState =
1930
+ state.conversationState && hasMatchingPrompt
1931
+ ? state.conversationState
1932
+ : create(ConversationStateStructureSchema, {
1933
+ rootPromptMessagesJson: [systemPromptId],
1934
+ turns: [],
1935
+ todos: [],
1936
+ pendingToolCalls: [],
1937
+ previousWorkspaceUris: [],
1938
+ fileStates: {},
1939
+ fileStatesV2: {},
1940
+ summaryArchives: [],
1941
+ turnTimings: [],
1942
+ subagentStates: {},
1943
+ selfSummaryCount: 0,
1944
+ readPaths: [],
1945
+ });
1946
+
1947
+ // Always populate turns from context.messages to ensure Cursor sees full conversation
1948
+ const conversationState = create(ConversationStateStructureSchema, {
1949
+ ...baseState,
1950
+ turns: turns.length > 0 ? turns : baseState.turns,
1951
+ });
1952
+
1953
+ const modelDetails = create(ModelDetailsSchema, {
1954
+ modelId: model.id,
1955
+ displayModelId: model.id,
1956
+ displayName: model.name,
1957
+ });
1958
+
1959
+ const runRequest = create(AgentRunRequestSchema, {
1960
+ conversationState,
1961
+ action,
1962
+ modelDetails,
1963
+ conversationId: state.conversationId,
1964
+ });
1965
+
1966
+ // Tools are sent later via requestContext (exec handshake)
1967
+
1968
+ if (options?.customSystemPrompt) {
1969
+ runRequest.customSystemPrompt = options.customSystemPrompt;
1970
+ }
1971
+
1972
+ const clientMessage = create(AgentClientMessageSchema, {
1973
+ message: { case: "runRequest", value: runRequest },
1974
+ });
1975
+
1976
+ const requestBytes = toBinary(AgentClientMessageSchema, clientMessage);
1977
+
1978
+ const toolNames = context.tools?.map((tool) => tool.name) ?? [];
1979
+ const detail =
1980
+ process.env.DEBUG_CURSOR === "2"
1981
+ ? ` ${JSON.stringify(clientMessage.message.value, debugReplacer, 2)?.slice(0, 2000)}`
1982
+ : "";
1983
+ log("info", "builtRunRequest", {
1984
+ bytes: requestBytes.length,
1985
+ tools: toolNames.length,
1986
+ toolNames: toolNames.slice(0, 20),
1987
+ detail: detail || undefined,
1988
+ });
1989
+
1990
+ return { requestBytes, blobStore, conversationState };
1991
+ }
1992
+
1993
+ function extractText(content: (TextContent | ImageContent)[]): string {
1994
+ return content
1995
+ .filter((c): c is TextContent => c.type === "text")
1996
+ .map((c) => c.text)
1997
+ .join("\n");
1998
+ }