pi-provider-cursor-ask 0.1.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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/README.zh-CN.md +87 -0
- package/UPSTREAM_CHANGELOG.md +368 -0
- package/UPSTREAM_SOURCE.md +23 -0
- package/dist/index.js +54 -0
- package/package.json +97 -0
- package/src/auth/cli-credentials.ts +275 -0
- package/src/auth/consent.ts +25 -0
- package/src/auth/index.ts +23 -0
- package/src/auth/oauth.ts +282 -0
- package/src/auth/refresh-guard.ts +93 -0
- package/src/client/bridge.ts +673 -0
- package/src/client/cursor-wire.ts +213 -0
- package/src/client/h2-unary.ts +142 -0
- package/src/client/index.ts +18 -0
- package/src/config/index.ts +69 -0
- package/src/diagnostics/diagnostics.ts +116 -0
- package/src/diagnostics/index.ts +1 -0
- package/src/extension/auth.ts +99 -0
- package/src/extension/commands.ts +163 -0
- package/src/extension/compaction-guard.ts +86 -0
- package/src/extension/debug-hooks.ts +359 -0
- package/src/extension/index.ts +8 -0
- package/src/extension/provider.ts +277 -0
- package/src/extension/quota-adapter.ts +175 -0
- package/src/extension/report-dashboard.ts +133 -0
- package/src/identity.ts +16 -0
- package/src/index.ts +186 -0
- package/src/models/ask-catalog.ts +384 -0
- package/src/models/catalog.json +1163 -0
- package/src/models/cost.ts +126 -0
- package/src/models/index.ts +6 -0
- package/src/models/limits.ts +36 -0
- package/src/models/parameterized.ts +416 -0
- package/src/models/processing.ts +313 -0
- package/src/proto/agent_pb.ts +14577 -0
- package/src/stream/bridge-session.ts +215 -0
- package/src/stream/client-transcript.ts +51 -0
- package/src/stream/config.ts +5 -0
- package/src/stream/context-normalize.ts +308 -0
- package/src/stream/context-usage.ts +168 -0
- package/src/stream/debug-log.ts +316 -0
- package/src/stream/drift.ts +122 -0
- package/src/stream/images.ts +201 -0
- package/src/stream/index.ts +68 -0
- package/src/stream/interaction-query.ts +369 -0
- package/src/stream/message-parsing.ts +402 -0
- package/src/stream/model-cache.ts +100 -0
- package/src/stream/model-discovery.ts +242 -0
- package/src/stream/model-routing.ts +100 -0
- package/src/stream/native-core.ts +2121 -0
- package/src/stream/pi-adapter.ts +414 -0
- package/src/stream/protocol.ts +63 -0
- package/src/stream/recovery.ts +494 -0
- package/src/stream/request-build.ts +668 -0
- package/src/stream/root-prompt.ts +184 -0
- package/src/stream/run-journal.ts +474 -0
- package/src/stream/run-usage.ts +107 -0
- package/src/stream/server-messages.ts +777 -0
- package/src/stream/session-state.ts +499 -0
- package/src/stream/stream-writer.ts +211 -0
- package/src/stream/thinking-filter.ts +63 -0
- package/src/stream/tool-schema.ts +185 -0
- package/src/stream/transport-errors.ts +150 -0
- package/src/stream/tuning.ts +250 -0
- package/src/stream/types.ts +330 -0
- package/src/types/enums.ts +103 -0
- package/src/types/index.ts +4 -0
- package/src/usage.ts +262 -0
- package/src/utils/cache-dir.ts +39 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/security.ts +68 -0
- package/src/utils/util.ts +43 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the `AgentRunRequest` protobuf that starts (or resumes) a Cursor turn.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes come out of here:
|
|
5
|
+
* - a fresh request carrying the full conversation history as turn structures
|
|
6
|
+
* - a resume request carrying an upstream checkpoint blob instead
|
|
7
|
+
*
|
|
8
|
+
* Large payloads (images, turn-step bytes) are content-addressed into a blob
|
|
9
|
+
* store and referenced by hash, which is what keeps a long session's request
|
|
10
|
+
* from re-uploading every attachment on every turn.
|
|
11
|
+
*/
|
|
12
|
+
import { create, fromBinary, fromJson, toBinary, toJson, type JsonValue } from "@bufbuild/protobuf";
|
|
13
|
+
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
AgentClientMessageSchema,
|
|
19
|
+
AgentConversationTurnStructureSchema,
|
|
20
|
+
AgentRunRequestSchema,
|
|
21
|
+
AssistantMessageSchema,
|
|
22
|
+
ConversationActionSchema,
|
|
23
|
+
ConversationStateStructureSchema,
|
|
24
|
+
ConversationStepSchema,
|
|
25
|
+
ConversationTurnStructureSchema,
|
|
26
|
+
McpArgsSchema,
|
|
27
|
+
McpImageContentSchema,
|
|
28
|
+
McpSuccessSchema,
|
|
29
|
+
McpTextContentSchema,
|
|
30
|
+
McpToolCallSchema,
|
|
31
|
+
McpToolErrorSchema,
|
|
32
|
+
McpToolResultSchema,
|
|
33
|
+
McpToolResultContentItemSchema,
|
|
34
|
+
McpToolsSchema,
|
|
35
|
+
RequestedModelSchema,
|
|
36
|
+
RequestedModel_ModelParameterbytesSchema,
|
|
37
|
+
SelectedContextSchema,
|
|
38
|
+
SelectedImageSchema,
|
|
39
|
+
ThinkingMessageSchema,
|
|
40
|
+
ToolCallSchema,
|
|
41
|
+
UserMessageActionSchema,
|
|
42
|
+
UserMessageSchema,
|
|
43
|
+
type McpToolDefinition,
|
|
44
|
+
type UserMessage,
|
|
45
|
+
} from "../proto/agent_pb.js";
|
|
46
|
+
import { buildSelectedContextBlob, type CursorModelParameter } from "../client/cursor-wire.js";
|
|
47
|
+
import { debugLog, requestDebugByBody } from "./debug-log.js";
|
|
48
|
+
import {
|
|
49
|
+
buildRootPromptMessages,
|
|
50
|
+
encodeRootPromptMessage,
|
|
51
|
+
isPromptHistoryEnabled,
|
|
52
|
+
systemPromptRootMessage,
|
|
53
|
+
} from "./root-prompt.js";
|
|
54
|
+
export {
|
|
55
|
+
buildMcpToolDefinitions,
|
|
56
|
+
isSlimToolsEnabled,
|
|
57
|
+
slimOpenAIToolsForCursor,
|
|
58
|
+
} from "./tool-schema.js";
|
|
59
|
+
import type {
|
|
60
|
+
CursorRequestPayload,
|
|
61
|
+
OpenAIToolDef,
|
|
62
|
+
ParsedImageContent,
|
|
63
|
+
ParsedToolResult,
|
|
64
|
+
ParsedTurn,
|
|
65
|
+
ParsedTurnStep,
|
|
66
|
+
} from "./types.js";
|
|
67
|
+
|
|
68
|
+
export const MAX_MCP_TOOL_TEXT_BYTES = 512 * 1024;
|
|
69
|
+
export const MAX_MCP_TOOL_RESULT_BYTES = 16 * 1024 * 1024;
|
|
70
|
+
|
|
71
|
+
function truncateUtf8(text: string, maxBytes: number, originalBytes: number): string {
|
|
72
|
+
const suffix = `\n\n[pi-cursor truncated this tool result from ${originalBytes} bytes to protect the agent context. Use a narrower command, path, or line range.]`;
|
|
73
|
+
const suffixBytes = Buffer.byteLength(suffix, "utf8");
|
|
74
|
+
const bytes = Buffer.from(text, "utf8");
|
|
75
|
+
let end = Math.max(0, maxBytes - suffixBytes);
|
|
76
|
+
while (end > 0 && end < bytes.length && (bytes[end]! & 0xc0) === 0x80) end -= 1;
|
|
77
|
+
return bytes.subarray(0, end).toString("utf8") + suffix;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Bound a single tool response before it is journaled or encoded for Cursor.
|
|
82
|
+
* An accidental recursive dump should not poison every later turn in a session.
|
|
83
|
+
*/
|
|
84
|
+
export function normalizeToolResultForTransport(
|
|
85
|
+
result: Omit<ParsedToolResult, "isError"> & { isError?: boolean },
|
|
86
|
+
): ParsedToolResult {
|
|
87
|
+
const originalTextBytes = Buffer.byteLength(result.content, "utf8");
|
|
88
|
+
let content =
|
|
89
|
+
originalTextBytes > MAX_MCP_TOOL_TEXT_BYTES
|
|
90
|
+
? truncateUtf8(result.content, MAX_MCP_TOOL_TEXT_BYTES, originalTextBytes)
|
|
91
|
+
: result.content;
|
|
92
|
+
|
|
93
|
+
const images: ParsedImageContent[] = [];
|
|
94
|
+
let usedBytes = Buffer.byteLength(content, "utf8");
|
|
95
|
+
let droppedImages = 0;
|
|
96
|
+
let droppedImageBytes = 0;
|
|
97
|
+
for (const image of result.images ?? []) {
|
|
98
|
+
const imageBytes = image.data.byteLength + Buffer.byteLength(image.mimeType, "utf8");
|
|
99
|
+
if (usedBytes + imageBytes > MAX_MCP_TOOL_RESULT_BYTES) {
|
|
100
|
+
droppedImages += 1;
|
|
101
|
+
droppedImageBytes += image.data.byteLength;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
images.push(image);
|
|
105
|
+
usedBytes += imageBytes;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (droppedImages > 0) {
|
|
109
|
+
const notice = `\n\n[pi-cursor omitted ${droppedImages} oversized tool image(s), totaling ${droppedImageBytes} bytes, to protect the transport.]`;
|
|
110
|
+
const combined = content + notice;
|
|
111
|
+
const combinedBytes = Buffer.byteLength(combined, "utf8");
|
|
112
|
+
content =
|
|
113
|
+
combinedBytes > MAX_MCP_TOOL_TEXT_BYTES
|
|
114
|
+
? truncateUtf8(combined, MAX_MCP_TOOL_TEXT_BYTES, combinedBytes)
|
|
115
|
+
: combined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
content,
|
|
120
|
+
isError: result.isError === true,
|
|
121
|
+
...(images.length > 0 && { images }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Turns whose answer depends on nothing but politeness. Pi's agent prompt adds
|
|
127
|
+
* nothing to "thanks", so it can be dropped along with the tools.
|
|
128
|
+
*/
|
|
129
|
+
const PLEASANTRY_CONVERSATIONAL_TURNS = new Set([
|
|
130
|
+
"hi",
|
|
131
|
+
"hello",
|
|
132
|
+
"hey",
|
|
133
|
+
"hi there",
|
|
134
|
+
"hello there",
|
|
135
|
+
"hey there",
|
|
136
|
+
"yo",
|
|
137
|
+
"how are you",
|
|
138
|
+
"whats up",
|
|
139
|
+
"good morning",
|
|
140
|
+
"good afternoon",
|
|
141
|
+
"good evening",
|
|
142
|
+
"thanks",
|
|
143
|
+
"thank you",
|
|
144
|
+
"thanks a lot",
|
|
145
|
+
"thank you very much",
|
|
146
|
+
"thx",
|
|
147
|
+
"ty",
|
|
148
|
+
"ok",
|
|
149
|
+
"okay",
|
|
150
|
+
"got it",
|
|
151
|
+
"sounds good",
|
|
152
|
+
"cool",
|
|
153
|
+
"great",
|
|
154
|
+
"nice",
|
|
155
|
+
"ping",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Identity and capability questions. Tool-free like a pleasantry, but the system
|
|
160
|
+
* prompt *is* the answer here — drop it and the model introduces itself as
|
|
161
|
+
* Cursor. Tools go; the prompt stays.
|
|
162
|
+
*/
|
|
163
|
+
const IDENTITY_CONVERSATIONAL_TURNS = new Set([
|
|
164
|
+
"what can you do",
|
|
165
|
+
"what can you do for me",
|
|
166
|
+
"what can you help me with",
|
|
167
|
+
"who are you",
|
|
168
|
+
"tell me about yourself",
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const TRIVIAL_CONVERSATIONAL_TURNS = new Set([
|
|
172
|
+
...PLEASANTRY_CONVERSATIONAL_TURNS,
|
|
173
|
+
...IDENTITY_CONVERSATIONAL_TURNS,
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
function normalizeConversationalTurn(text: string): string {
|
|
177
|
+
return text
|
|
178
|
+
.trim()
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[’']/g, "")
|
|
181
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
182
|
+
.replace(/\s+/g, " ")
|
|
183
|
+
.trim();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Narrow allowlist for turns that cannot reasonably require a tool. Exact
|
|
188
|
+
* matching is intentional: "hi, inspect src" must retain the full tool set.
|
|
189
|
+
*/
|
|
190
|
+
export function isTrivialConversationalTurn(text: string): boolean {
|
|
191
|
+
const normalized = normalizeConversationalTurn(text);
|
|
192
|
+
return normalized.length <= 40 && TRIVIAL_CONVERSATIONAL_TURNS.has(normalized);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Subset of trivial turns the system prompt itself answers. These keep the full
|
|
197
|
+
* prompt even though they still drop tools.
|
|
198
|
+
*/
|
|
199
|
+
export function isIdentityConversationalTurn(text: string): boolean {
|
|
200
|
+
const normalized = normalizeConversationalTurn(text);
|
|
201
|
+
return normalized.length <= 40 && IDENTITY_CONVERSATIONAL_TURNS.has(normalized);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function summarizeRequestSize(input: {
|
|
205
|
+
systemPrompt: string;
|
|
206
|
+
userText: string;
|
|
207
|
+
tools: OpenAIToolDef[];
|
|
208
|
+
mcpTools: McpToolDefinition[];
|
|
209
|
+
requestBytes: Uint8Array;
|
|
210
|
+
blobStore: Map<string, Uint8Array>;
|
|
211
|
+
turnCount?: number;
|
|
212
|
+
}): {
|
|
213
|
+
systemChars: number;
|
|
214
|
+
userChars: number;
|
|
215
|
+
toolCount: number;
|
|
216
|
+
toolJsonChars: number;
|
|
217
|
+
mcpSchemaBytes: number;
|
|
218
|
+
requestBytes: number;
|
|
219
|
+
blobBytes: number;
|
|
220
|
+
wireBytes: number;
|
|
221
|
+
turnCount: number;
|
|
222
|
+
approxInputTokens: number;
|
|
223
|
+
} {
|
|
224
|
+
let blobBytes = 0;
|
|
225
|
+
for (const bytes of input.blobStore.values()) blobBytes += bytes.byteLength;
|
|
226
|
+
let mcpSchemaBytes = 0;
|
|
227
|
+
for (const tool of input.mcpTools) {
|
|
228
|
+
mcpSchemaBytes += tool.inputSchema?.byteLength ?? 0;
|
|
229
|
+
mcpSchemaBytes += (tool.description?.length ?? 0) + (tool.name?.length ?? 0);
|
|
230
|
+
}
|
|
231
|
+
// `toolJsonChars` reports the raw Pi surface for comparison only. Do not add
|
|
232
|
+
// it (or mcpSchemaBytes) to the estimate: the encoded schemas already live in
|
|
233
|
+
// requestBytes, and the system/conversation content already lives in blobs.
|
|
234
|
+
const toolJsonChars = JSON.stringify(input.tools).length;
|
|
235
|
+
const systemChars = input.systemPrompt.length;
|
|
236
|
+
const userChars = input.userText.length;
|
|
237
|
+
const wireBytes = input.requestBytes.byteLength + blobBytes;
|
|
238
|
+
// Rough UTF-8/Latin heuristic used only for diagnostics, not billing.
|
|
239
|
+
const approxInputTokens = Math.round(wireBytes / 4);
|
|
240
|
+
return {
|
|
241
|
+
systemChars,
|
|
242
|
+
userChars,
|
|
243
|
+
toolCount: input.tools.length,
|
|
244
|
+
toolJsonChars,
|
|
245
|
+
mcpSchemaBytes,
|
|
246
|
+
requestBytes: input.requestBytes.byteLength,
|
|
247
|
+
blobBytes,
|
|
248
|
+
wireBytes,
|
|
249
|
+
turnCount: input.turnCount ?? 0,
|
|
250
|
+
approxInputTokens,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function decodeMcpArgValue(value: Uint8Array): unknown {
|
|
255
|
+
try {
|
|
256
|
+
const parsed = fromBinary(ValueSchema, value);
|
|
257
|
+
return toJson(ValueSchema, parsed);
|
|
258
|
+
} catch {
|
|
259
|
+
// Not a protobuf Value; treat bytes as UTF-8 text for MCP tool args.
|
|
260
|
+
return new TextDecoder().decode(value);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function decodeMcpArgsMap(args: Record<string, Uint8Array>): Record<string, unknown> {
|
|
265
|
+
const decoded: Record<string, unknown> = {};
|
|
266
|
+
for (const [key, value] of Object.entries(args)) decoded[key] = decodeMcpArgValue(value);
|
|
267
|
+
return decoded;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function encodeMcpArgValue(value: unknown): Uint8Array {
|
|
271
|
+
try {
|
|
272
|
+
return toBinary(ValueSchema, fromJson(ValueSchema, value as JsonValue));
|
|
273
|
+
} catch {
|
|
274
|
+
return new TextEncoder().encode(String(value));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function encodeMcpArgsMap(args: Record<string, unknown>): Record<string, Uint8Array> {
|
|
279
|
+
const encoded: Record<string, Uint8Array> = {};
|
|
280
|
+
for (const [key, value] of Object.entries(args)) encoded[key] = encodeMcpArgValue(value);
|
|
281
|
+
return encoded;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function storeAsBlob(data: Uint8Array, blobStore: Map<string, Uint8Array>): Uint8Array {
|
|
285
|
+
const id = new Uint8Array(createHash("sha256").update(data).digest());
|
|
286
|
+
blobStore.set(Buffer.from(id).toString("hex"), data);
|
|
287
|
+
return id;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createSelectedImages(images: ParsedImageContent[]) {
|
|
291
|
+
// Matches Cursor CLI's ACP image path for inline image data:
|
|
292
|
+
// new SelectedImage({ dataOrBlobId: { case: "data", value }, uuid, mimeType })
|
|
293
|
+
return images.map((image) =>
|
|
294
|
+
create(SelectedImageSchema, {
|
|
295
|
+
uuid: crypto.randomUUID(),
|
|
296
|
+
mimeType: image.mimeType,
|
|
297
|
+
dataOrBlobId: { case: "data", value: image.data },
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function createUserMessage(
|
|
303
|
+
text: string,
|
|
304
|
+
selectedContextBlob: Uint8Array,
|
|
305
|
+
images: ParsedImageContent[] = [],
|
|
306
|
+
): UserMessage {
|
|
307
|
+
const messageId = crypto.randomUUID();
|
|
308
|
+
return create(UserMessageSchema, {
|
|
309
|
+
text,
|
|
310
|
+
messageId,
|
|
311
|
+
selectedContext: create(SelectedContextSchema, {
|
|
312
|
+
selectedImages: createSelectedImages(images),
|
|
313
|
+
}),
|
|
314
|
+
mode: 1,
|
|
315
|
+
selectedContextBlob,
|
|
316
|
+
correlationId: messageId,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function buildMcpSuccessContent(result: ParsedToolResult) {
|
|
321
|
+
result = normalizeToolResultForTransport(result);
|
|
322
|
+
const content = [];
|
|
323
|
+
if (result.content.length > 0) {
|
|
324
|
+
content.push(
|
|
325
|
+
create(McpToolResultContentItemSchema, {
|
|
326
|
+
content: {
|
|
327
|
+
case: "text",
|
|
328
|
+
value: create(McpTextContentSchema, { text: result.content }),
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
for (const image of result.images ?? []) {
|
|
334
|
+
content.push(
|
|
335
|
+
create(McpToolResultContentItemSchema, {
|
|
336
|
+
content: {
|
|
337
|
+
case: "image",
|
|
338
|
+
value: create(McpImageContentSchema, { data: image.data, mimeType: image.mimeType }),
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (content.length === 0) {
|
|
344
|
+
content.push(
|
|
345
|
+
create(McpToolResultContentItemSchema, {
|
|
346
|
+
content: { case: "text", value: create(McpTextContentSchema, { text: "" }) },
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return content;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function buildTurnStepBytes(step: ParsedTurnStep): Uint8Array {
|
|
354
|
+
if (step.kind === "assistantText") {
|
|
355
|
+
return toBinary(
|
|
356
|
+
ConversationStepSchema,
|
|
357
|
+
create(ConversationStepSchema, {
|
|
358
|
+
message: {
|
|
359
|
+
case: "assistantMessage",
|
|
360
|
+
value: create(AssistantMessageSchema, { text: step.text }),
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (step.kind === "thinking") {
|
|
367
|
+
return toBinary(
|
|
368
|
+
ConversationStepSchema,
|
|
369
|
+
create(ConversationStepSchema, {
|
|
370
|
+
message: {
|
|
371
|
+
case: "thinkingMessage",
|
|
372
|
+
value: create(ThinkingMessageSchema, { text: step.text }),
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const toolName = step.toolName || "tool";
|
|
379
|
+
const mcpToolCall = create(McpToolCallSchema, {
|
|
380
|
+
args: create(McpArgsSchema, {
|
|
381
|
+
name: toolName,
|
|
382
|
+
args: encodeMcpArgsMap(step.arguments),
|
|
383
|
+
toolCallId: step.toolCallId,
|
|
384
|
+
providerIdentifier: "pi",
|
|
385
|
+
toolName,
|
|
386
|
+
}),
|
|
387
|
+
...(step.result && {
|
|
388
|
+
result: create(McpToolResultSchema, {
|
|
389
|
+
result: step.result.isError
|
|
390
|
+
? {
|
|
391
|
+
case: "error",
|
|
392
|
+
value: create(McpToolErrorSchema, { error: step.result.content }),
|
|
393
|
+
}
|
|
394
|
+
: {
|
|
395
|
+
case: "success",
|
|
396
|
+
value: create(McpSuccessSchema, {
|
|
397
|
+
content: buildMcpSuccessContent(step.result),
|
|
398
|
+
isError: false,
|
|
399
|
+
}),
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return toBinary(
|
|
406
|
+
ConversationStepSchema,
|
|
407
|
+
create(ConversationStepSchema, {
|
|
408
|
+
message: {
|
|
409
|
+
case: "toolCall",
|
|
410
|
+
value: create(ToolCallSchema, {
|
|
411
|
+
tool: {
|
|
412
|
+
case: "mcpToolCall",
|
|
413
|
+
value: mcpToolCall,
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export type BuildCursorRequestImageInput =
|
|
422
|
+
| ParsedImageContent
|
|
423
|
+
| {
|
|
424
|
+
data: string;
|
|
425
|
+
mimeType: string;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
export interface BuildCursorRequestTurnInput extends Omit<ParsedTurn, "userImages"> {
|
|
429
|
+
images?: BuildCursorRequestImageInput[];
|
|
430
|
+
userImages?: BuildCursorRequestImageInput[];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface BuildCursorRequestOptions {
|
|
434
|
+
checkpoint: Uint8Array | null;
|
|
435
|
+
conversationId: string;
|
|
436
|
+
cursorModelParameters?: CursorModelParameter[];
|
|
437
|
+
existingBlobStore?: Map<string, Uint8Array>;
|
|
438
|
+
mcpTools?: McpToolDefinition[];
|
|
439
|
+
modelId: string;
|
|
440
|
+
systemPrompt: string;
|
|
441
|
+
turns?: BuildCursorRequestTurnInput[];
|
|
442
|
+
userImages?: BuildCursorRequestImageInput[];
|
|
443
|
+
userText?: string;
|
|
444
|
+
maxMode?: boolean;
|
|
445
|
+
/** Re-publish the system prompt onto a checkpoint whose recorded prompt is stale. */
|
|
446
|
+
refreshSystemPrompt?: boolean;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function normalizeImageInput(image: BuildCursorRequestImageInput): ParsedImageContent {
|
|
450
|
+
if (image.data instanceof Uint8Array) {
|
|
451
|
+
return {
|
|
452
|
+
data: image.data,
|
|
453
|
+
mimeType: image.mimeType,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
data: new Uint8Array(Buffer.from(image.data.replace(/\s/g, ""), "base64")),
|
|
458
|
+
mimeType: image.mimeType,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function normalizeTurnInput(turn: BuildCursorRequestTurnInput): ParsedTurn {
|
|
463
|
+
const images = turn.userImages ?? turn.images;
|
|
464
|
+
return {
|
|
465
|
+
userText: turn.userText,
|
|
466
|
+
steps: turn.steps,
|
|
467
|
+
...(images && images.length > 0 ? { userImages: images.map(normalizeImageInput) } : {}),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function buildCursorRequest(
|
|
472
|
+
modelOrOptions: string | BuildCursorRequestOptions,
|
|
473
|
+
systemPrompt?: string,
|
|
474
|
+
userText?: string,
|
|
475
|
+
turns?: ParsedTurn[],
|
|
476
|
+
conversationId?: string,
|
|
477
|
+
checkpoint?: Uint8Array | null,
|
|
478
|
+
existingBlobStore?: Map<string, Uint8Array>,
|
|
479
|
+
maxMode = false,
|
|
480
|
+
cursorModelParameters: CursorModelParameter[] = [],
|
|
481
|
+
mcpTools: McpToolDefinition[] = [],
|
|
482
|
+
userImages: ParsedImageContent[] = [],
|
|
483
|
+
refreshSystemPrompt = false,
|
|
484
|
+
): CursorRequestPayload {
|
|
485
|
+
if (typeof modelOrOptions !== "string") {
|
|
486
|
+
const normalizedTurns = (modelOrOptions.turns ?? []).map(normalizeTurnInput);
|
|
487
|
+
const currentTurn =
|
|
488
|
+
modelOrOptions.userText === undefined && normalizedTurns.length > 0
|
|
489
|
+
? normalizedTurns[normalizedTurns.length - 1]
|
|
490
|
+
: undefined;
|
|
491
|
+
const completedTurns = currentTurn ? normalizedTurns.slice(0, -1) : normalizedTurns;
|
|
492
|
+
const currentImages = modelOrOptions.userImages
|
|
493
|
+
? modelOrOptions.userImages.map(normalizeImageInput)
|
|
494
|
+
: (currentTurn?.userImages ?? []);
|
|
495
|
+
|
|
496
|
+
return buildCursorRequestFromParts(
|
|
497
|
+
modelOrOptions.modelId,
|
|
498
|
+
modelOrOptions.systemPrompt,
|
|
499
|
+
modelOrOptions.userText ?? currentTurn?.userText ?? "",
|
|
500
|
+
completedTurns,
|
|
501
|
+
modelOrOptions.conversationId,
|
|
502
|
+
modelOrOptions.checkpoint,
|
|
503
|
+
modelOrOptions.existingBlobStore,
|
|
504
|
+
modelOrOptions.maxMode ?? false,
|
|
505
|
+
modelOrOptions.cursorModelParameters ?? [],
|
|
506
|
+
modelOrOptions.mcpTools ?? [],
|
|
507
|
+
currentImages,
|
|
508
|
+
modelOrOptions.refreshSystemPrompt ?? false,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return buildCursorRequestFromParts(
|
|
513
|
+
modelOrOptions,
|
|
514
|
+
systemPrompt ?? "",
|
|
515
|
+
userText ?? "",
|
|
516
|
+
turns ?? [],
|
|
517
|
+
conversationId ?? crypto.randomUUID(),
|
|
518
|
+
checkpoint ?? null,
|
|
519
|
+
existingBlobStore,
|
|
520
|
+
maxMode,
|
|
521
|
+
cursorModelParameters,
|
|
522
|
+
mcpTools,
|
|
523
|
+
userImages,
|
|
524
|
+
refreshSystemPrompt,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function buildCursorRequestFromParts(
|
|
529
|
+
modelId: string,
|
|
530
|
+
systemPrompt: string,
|
|
531
|
+
userText: string,
|
|
532
|
+
turns: ParsedTurn[],
|
|
533
|
+
conversationId: string,
|
|
534
|
+
checkpoint: Uint8Array | null,
|
|
535
|
+
existingBlobStore?: Map<string, Uint8Array>,
|
|
536
|
+
maxMode = false,
|
|
537
|
+
cursorModelParameters: CursorModelParameter[] = [],
|
|
538
|
+
mcpTools: McpToolDefinition[] = [],
|
|
539
|
+
userImages: ParsedImageContent[] = [],
|
|
540
|
+
refreshSystemPrompt = false,
|
|
541
|
+
): CursorRequestPayload {
|
|
542
|
+
debugLog("cursor_request.build.start", {
|
|
543
|
+
modelId,
|
|
544
|
+
systemPrompt,
|
|
545
|
+
userText,
|
|
546
|
+
turns,
|
|
547
|
+
conversationId,
|
|
548
|
+
checkpoint,
|
|
549
|
+
existingBlobStore,
|
|
550
|
+
maxMode,
|
|
551
|
+
cursorModelParameters,
|
|
552
|
+
mcpToolCount: mcpTools.length,
|
|
553
|
+
userImageCount: userImages.length,
|
|
554
|
+
});
|
|
555
|
+
const blobStore = new Map<string, Uint8Array>(existingBlobStore ?? []);
|
|
556
|
+
|
|
557
|
+
const systemBytes = new TextEncoder().encode(
|
|
558
|
+
JSON.stringify({ role: "system", content: systemPrompt }),
|
|
559
|
+
);
|
|
560
|
+
const systemBlobId = storeAsBlob(systemBytes, blobStore);
|
|
561
|
+
const selectedCtxBlob = storeAsBlob(buildSelectedContextBlob([systemBlobId], "pi"), blobStore);
|
|
562
|
+
|
|
563
|
+
let conversationState;
|
|
564
|
+
if (checkpoint) {
|
|
565
|
+
conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
|
|
566
|
+
// A checkpoint froze the instructions recorded when the conversation began.
|
|
567
|
+
// Pi rewrites its system prompt as a session evolves — context-mode folds
|
|
568
|
+
// session memory into it — so a changed prompt is re-published here instead
|
|
569
|
+
// of being silently pinned to whatever turn one happened to say.
|
|
570
|
+
if (refreshSystemPrompt && isPromptHistoryEnabled() && systemPrompt.trim()) {
|
|
571
|
+
conversationState.rootPromptMessagesJson = [
|
|
572
|
+
...conversationState.rootPromptMessagesJson,
|
|
573
|
+
storeAsBlob(encodeRootPromptMessage(systemPromptRootMessage(systemPrompt)), blobStore),
|
|
574
|
+
];
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
const turnBlobIds: Uint8Array[] = [];
|
|
578
|
+
for (const turn of turns) {
|
|
579
|
+
const userMsg = createUserMessage(turn.userText, selectedCtxBlob, turn.userImages ?? []);
|
|
580
|
+
const userMsgBlobId = storeAsBlob(toBinary(UserMessageSchema, userMsg), blobStore);
|
|
581
|
+
const stepBlobIds = turn.steps.map((s) => storeAsBlob(buildTurnStepBytes(s), blobStore));
|
|
582
|
+
|
|
583
|
+
const agentTurn = create(AgentConversationTurnStructureSchema, {
|
|
584
|
+
userMessage: userMsgBlobId,
|
|
585
|
+
steps: stepBlobIds,
|
|
586
|
+
requestId: crypto.randomUUID(),
|
|
587
|
+
});
|
|
588
|
+
const turnStructure = create(ConversationTurnStructureSchema, {
|
|
589
|
+
turn: { case: "agentConversationTurn", value: agentTurn },
|
|
590
|
+
});
|
|
591
|
+
turnBlobIds.push(
|
|
592
|
+
storeAsBlob(toBinary(ConversationTurnStructureSchema, turnStructure), blobStore),
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// `turns` is state, not prompt: Cursor's server never renders it back into
|
|
597
|
+
// model messages. The prompt it actually reads is this list, so the system
|
|
598
|
+
// prompt and every completed turn are replayed here — see ./root-prompt.ts.
|
|
599
|
+
const promptBlobIds = isPromptHistoryEnabled()
|
|
600
|
+
? buildRootPromptMessages(systemPrompt, turns).map((message) =>
|
|
601
|
+
storeAsBlob(encodeRootPromptMessage(message), blobStore),
|
|
602
|
+
)
|
|
603
|
+
: [];
|
|
604
|
+
|
|
605
|
+
conversationState = create(ConversationStateStructureSchema, {
|
|
606
|
+
rootPromptMessagesJson: [systemBlobId, ...promptBlobIds],
|
|
607
|
+
turns: turnBlobIds,
|
|
608
|
+
todos: [],
|
|
609
|
+
pendingToolCalls: [],
|
|
610
|
+
previousWorkspaceUris: [pathToFileURL(process.cwd()).href],
|
|
611
|
+
mode: 1,
|
|
612
|
+
fileStates: {},
|
|
613
|
+
fileStatesV2: {},
|
|
614
|
+
summaryArchives: [],
|
|
615
|
+
turnTimings: [],
|
|
616
|
+
subagentStates: {},
|
|
617
|
+
selfSummaryCount: 0,
|
|
618
|
+
readPaths: [],
|
|
619
|
+
clientName: "pi",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const userMessage = createUserMessage(userText, selectedCtxBlob, userImages);
|
|
624
|
+
const action = create(ConversationActionSchema, {
|
|
625
|
+
action: { case: "userMessageAction", value: create(UserMessageActionSchema, { userMessage }) },
|
|
626
|
+
});
|
|
627
|
+
// Cursor's newer request path uses requestedModel instead of legacy modelDetails.
|
|
628
|
+
// Some Cursor models (for example GPT-5.5) use requestedModel.parameters
|
|
629
|
+
// for context/reasoning/fast instead of encoding everything in the model ID.
|
|
630
|
+
// Max Mode is routed from model metadata for parameterized variants.
|
|
631
|
+
debugLog("cursor_request.requested_model", {
|
|
632
|
+
modelId,
|
|
633
|
+
maxMode,
|
|
634
|
+
parameters: cursorModelParameters,
|
|
635
|
+
});
|
|
636
|
+
const parameters = cursorModelParameters.map((parameter) =>
|
|
637
|
+
create(RequestedModel_ModelParameterbytesSchema, parameter),
|
|
638
|
+
);
|
|
639
|
+
const requestedModel = create(RequestedModelSchema, { modelId, maxMode, parameters });
|
|
640
|
+
const runRequest = create(AgentRunRequestSchema, {
|
|
641
|
+
conversationState,
|
|
642
|
+
action,
|
|
643
|
+
requestedModel,
|
|
644
|
+
conversationId,
|
|
645
|
+
mcpTools: create(McpToolsSchema, { mcpTools }),
|
|
646
|
+
});
|
|
647
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
648
|
+
message: { case: "runRequest", value: runRequest },
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const requestBytes = toBinary(AgentClientMessageSchema, clientMessage);
|
|
652
|
+
const payload = {
|
|
653
|
+
contextCheckpoint: checkpoint ?? null,
|
|
654
|
+
requestBytes,
|
|
655
|
+
requestBody: requestBytes,
|
|
656
|
+
blobStore,
|
|
657
|
+
mcpTools,
|
|
658
|
+
};
|
|
659
|
+
requestDebugByBody.set(requestBytes, {
|
|
660
|
+
systemPrompt,
|
|
661
|
+
selectedImages: userImages.map((image) => ({
|
|
662
|
+
byteLength: image.data.byteLength,
|
|
663
|
+
mimeType: image.mimeType,
|
|
664
|
+
})),
|
|
665
|
+
});
|
|
666
|
+
debugLog("cursor_request.build.end", payload);
|
|
667
|
+
return payload;
|
|
668
|
+
}
|