@tt-a1i/openpi 0.5.0 → 0.6.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/README.md +18 -10
- package/SETUP.md +8 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +25 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +44 -4
- package/extensions/subagents/src/backends/pi.ts +76 -5
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +5 -0
- package/extensions/subagents/src/prompt.ts +17 -3
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +8 -0
- package/extensions/workflows/index.ts +13 -0
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +6 -71
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +3 -2
- package/skills/subagents/SKILL.md +1 -0
- package/skills/workflows/REFERENCE.md +3 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import * as http2 from "node:http2";
|
|
3
|
+
import type {
|
|
4
|
+
Api,
|
|
5
|
+
AssistantMessage,
|
|
6
|
+
AssistantMessageEventStream,
|
|
7
|
+
Context,
|
|
8
|
+
ImageContent,
|
|
9
|
+
Message,
|
|
10
|
+
Model,
|
|
11
|
+
SimpleStreamOptions,
|
|
12
|
+
TextContent,
|
|
13
|
+
} from "@earendil-works/pi-ai/compat";
|
|
14
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/compat";
|
|
15
|
+
import { emptyUsage } from "../usage.ts";
|
|
16
|
+
import {
|
|
17
|
+
CURSOR_API_URL,
|
|
18
|
+
CURSOR_CLIENT_VERSION,
|
|
19
|
+
CURSOR_RUN_PATH,
|
|
20
|
+
} from "./constants.ts";
|
|
21
|
+
import {
|
|
22
|
+
AgentClientMessageSchema,
|
|
23
|
+
AgentConversationTurnStructureSchema,
|
|
24
|
+
type AgentRunRequest,
|
|
25
|
+
AgentRunRequestSchema,
|
|
26
|
+
AgentServerMessageSchema,
|
|
27
|
+
AssistantMessageSchema,
|
|
28
|
+
ClientHeartbeatSchema,
|
|
29
|
+
ConversationActionSchema,
|
|
30
|
+
type ConversationStateStructure,
|
|
31
|
+
ConversationStateStructureSchema,
|
|
32
|
+
ConversationStepSchema,
|
|
33
|
+
ConversationTurnStructureSchema,
|
|
34
|
+
type CursorRule,
|
|
35
|
+
CursorRuleSchema,
|
|
36
|
+
CursorRuleTypeGlobalSchema,
|
|
37
|
+
CursorRuleTypeSchema,
|
|
38
|
+
ExecClientControlMessageSchema,
|
|
39
|
+
ExecClientMessageSchema,
|
|
40
|
+
ExecClientStreamCloseSchema,
|
|
41
|
+
ExecClientThrowSchema,
|
|
42
|
+
GetBlobResultSchema,
|
|
43
|
+
type InteractionUpdate,
|
|
44
|
+
KvClientMessageSchema,
|
|
45
|
+
type KvServerMessage,
|
|
46
|
+
KvServerMessageSchema,
|
|
47
|
+
type ModelDetails,
|
|
48
|
+
ModelDetailsSchema,
|
|
49
|
+
RequestContextResultSchema,
|
|
50
|
+
RequestContextSchema,
|
|
51
|
+
RequestContextSuccessSchema,
|
|
52
|
+
type RequestedModel_ModelParameterbytes,
|
|
53
|
+
RequestedModel_ModelParameterbytesSchema,
|
|
54
|
+
RequestedModelSchema,
|
|
55
|
+
ResumeActionSchema,
|
|
56
|
+
SelectedContextSchema,
|
|
57
|
+
SelectedImageSchema,
|
|
58
|
+
SetBlobResultSchema,
|
|
59
|
+
UserMessageActionSchema,
|
|
60
|
+
UserMessageSchema,
|
|
61
|
+
} from "./proto.ts";
|
|
62
|
+
import { create, fromBinary, toBinary } from "./protobuf.ts";
|
|
63
|
+
import { connectCursorHttp2 } from "./proxy.ts";
|
|
64
|
+
|
|
65
|
+
const CONNECT_END_STREAM_FLAG = 0b00000010;
|
|
66
|
+
const CONNECT_COMPRESSED_FLAG = 0b00000001;
|
|
67
|
+
const MAX_CONNECT_FRAME_BYTES = 16 * 1024 * 1024;
|
|
68
|
+
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
69
|
+
const PROXY_TUNNEL_TIMEOUT_MS = 30_000;
|
|
70
|
+
|
|
71
|
+
export const CURSOR_CHAT_ONLY_SYSTEM_PROMPT =
|
|
72
|
+
"This Cursor provider is running in chat-only mode. No filesystem, shell, code modification, MCP, web, or user-interaction tools are available. Never emit tool calls or interaction queries. Images attached to the user message are already available for direct analysis. If required information is unavailable, explain the limitation in text instead of attempting a tool.";
|
|
73
|
+
|
|
74
|
+
const HTTP2_FORBIDDEN_HEADERS = new Set([
|
|
75
|
+
"connection",
|
|
76
|
+
"keep-alive",
|
|
77
|
+
"proxy-connection",
|
|
78
|
+
"transfer-encoding",
|
|
79
|
+
"upgrade",
|
|
80
|
+
"http2-settings",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const CURSOR_RESERVED_HEADERS = new Set([
|
|
84
|
+
"content-type",
|
|
85
|
+
"connect-protocol-version",
|
|
86
|
+
"te",
|
|
87
|
+
"authorization",
|
|
88
|
+
"x-ghost-mode",
|
|
89
|
+
"x-cursor-client-version",
|
|
90
|
+
"x-cursor-client-type",
|
|
91
|
+
"x-request-id",
|
|
92
|
+
"host",
|
|
93
|
+
"content-length",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
type CursorBlobStore = Map<string, Uint8Array>;
|
|
97
|
+
|
|
98
|
+
export interface CursorRequestBuild {
|
|
99
|
+
request: AgentRunRequest;
|
|
100
|
+
requestBytes: Uint8Array;
|
|
101
|
+
blobStore: CursorBlobStore;
|
|
102
|
+
conversationState: ConversationStateStructure;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Connect's five-byte big-endian envelope. */
|
|
106
|
+
export function frameConnectMessage(data: Uint8Array, flags = 0): Buffer {
|
|
107
|
+
const frame = Buffer.allocUnsafe(5 + data.length);
|
|
108
|
+
frame[0] = flags;
|
|
109
|
+
frame.writeUInt32BE(data.length, 1);
|
|
110
|
+
frame.set(data, 5);
|
|
111
|
+
return frame;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createBlobId(data: Uint8Array): Uint8Array {
|
|
115
|
+
return new Uint8Array(createHash("sha256").update(data).digest());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function storeBlob(store: CursorBlobStore, data: Uint8Array): Uint8Array {
|
|
119
|
+
const id = createBlobId(data);
|
|
120
|
+
store.set(Buffer.from(id).toString("hex"), data);
|
|
121
|
+
return id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function textFromContent(
|
|
125
|
+
content: string | (TextContent | ImageContent)[],
|
|
126
|
+
): string {
|
|
127
|
+
if (typeof content === "string") return content.trim();
|
|
128
|
+
return content
|
|
129
|
+
.filter((item): item is TextContent => item.type === "text")
|
|
130
|
+
.map((item) => item.text)
|
|
131
|
+
.join("\n")
|
|
132
|
+
.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function imagesFromContent(content: string | (TextContent | ImageContent)[]) {
|
|
136
|
+
if (typeof content === "string") return [];
|
|
137
|
+
return content
|
|
138
|
+
.filter((item): item is ImageContent => item.type === "image")
|
|
139
|
+
.map((item) =>
|
|
140
|
+
create(SelectedImageSchema, {
|
|
141
|
+
uuid: randomUUID(),
|
|
142
|
+
path: "",
|
|
143
|
+
mimeType: item.mimeType,
|
|
144
|
+
dataOrBlobId: {
|
|
145
|
+
case: "data",
|
|
146
|
+
value: Uint8Array.from(Buffer.from(item.data, "base64")),
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function userMessageFromContent(
|
|
153
|
+
content: string | (TextContent | ImageContent)[],
|
|
154
|
+
messageId = randomUUID(),
|
|
155
|
+
) {
|
|
156
|
+
const text = textFromContent(content);
|
|
157
|
+
const images = imagesFromContent(content);
|
|
158
|
+
return create(UserMessageSchema, {
|
|
159
|
+
text,
|
|
160
|
+
messageId,
|
|
161
|
+
...(images.length > 0
|
|
162
|
+
? {
|
|
163
|
+
selectedContext: create(SelectedContextSchema, {
|
|
164
|
+
selectedImages: images,
|
|
165
|
+
}),
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function rootPromptContent(
|
|
172
|
+
content: string | (TextContent | ImageContent)[],
|
|
173
|
+
): Array<
|
|
174
|
+
| { type: "text"; text: string }
|
|
175
|
+
| { type: "image"; image: string; mediaType: string }
|
|
176
|
+
> {
|
|
177
|
+
if (typeof content === "string") {
|
|
178
|
+
const text = content.trim();
|
|
179
|
+
return text ? [{ type: "text", text }] : [];
|
|
180
|
+
}
|
|
181
|
+
const parts: Array<
|
|
182
|
+
| { type: "text"; text: string }
|
|
183
|
+
| { type: "image"; image: string; mediaType: string }
|
|
184
|
+
> = [];
|
|
185
|
+
for (const item of content) {
|
|
186
|
+
if (item.type === "text") {
|
|
187
|
+
const text = item.text.trim();
|
|
188
|
+
if (text) parts.push({ type: "text", text });
|
|
189
|
+
} else {
|
|
190
|
+
parts.push({
|
|
191
|
+
type: "image",
|
|
192
|
+
image: `data:${item.mimeType};base64,${item.data}`,
|
|
193
|
+
mediaType: item.mimeType,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return parts;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assistantRootContent(
|
|
201
|
+
message: Extract<Message, { role: "assistant" }>,
|
|
202
|
+
) {
|
|
203
|
+
const content: Array<Record<string, unknown>> = [];
|
|
204
|
+
for (const item of message.content) {
|
|
205
|
+
if (item.type === "text" && item.text) {
|
|
206
|
+
content.push({ type: "text", text: item.text });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return content;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildHistoryRootPrompt(
|
|
213
|
+
messages: Message[],
|
|
214
|
+
store: CursorBlobStore,
|
|
215
|
+
activeUserIndex: number,
|
|
216
|
+
): Uint8Array[] {
|
|
217
|
+
const entries: Uint8Array[] = [];
|
|
218
|
+
for (let index = 0; index < messages.length; index++) {
|
|
219
|
+
if (index === activeUserIndex) break;
|
|
220
|
+
const message = messages[index];
|
|
221
|
+
let value: unknown;
|
|
222
|
+
if (message.role === "user") {
|
|
223
|
+
const content = rootPromptContent(message.content);
|
|
224
|
+
if (content.length === 0) continue;
|
|
225
|
+
value = { role: "user", content };
|
|
226
|
+
} else if (message.role === "assistant") {
|
|
227
|
+
const content = assistantRootContent(message);
|
|
228
|
+
if (content.length === 0) continue;
|
|
229
|
+
value = { role: "assistant", content };
|
|
230
|
+
} else {
|
|
231
|
+
// Chat-only mode never replays assistant tool calls. Replaying only the
|
|
232
|
+
// matching tool result would create an invalid orphan in Cursor history.
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
entries.push(
|
|
236
|
+
storeBlob(store, new TextEncoder().encode(JSON.stringify(value))),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return entries;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildSystemPrompt(
|
|
243
|
+
systemPrompt: Context["systemPrompt"],
|
|
244
|
+
store: CursorBlobStore,
|
|
245
|
+
): Uint8Array[] {
|
|
246
|
+
const prompts = systemPrompt
|
|
247
|
+
? Array.isArray(systemPrompt)
|
|
248
|
+
? systemPrompt
|
|
249
|
+
: [systemPrompt]
|
|
250
|
+
: ["You are a helpful assistant."];
|
|
251
|
+
return [...prompts, CURSOR_CHAT_ONLY_SYSTEM_PROMPT].map((prompt) =>
|
|
252
|
+
storeBlob(
|
|
253
|
+
store,
|
|
254
|
+
new TextEncoder().encode(
|
|
255
|
+
JSON.stringify({ role: "system", content: prompt }),
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Cursor asks for these rules over the exec channel before generating text.
|
|
263
|
+
* They are global rules only; the chat-only provider intentionally returns an
|
|
264
|
+
* empty MCP tool list and never forwards `context.tools`.
|
|
265
|
+
*/
|
|
266
|
+
export function buildCursorRequestContextRules(
|
|
267
|
+
systemPrompt: Context["systemPrompt"],
|
|
268
|
+
): CursorRule[] {
|
|
269
|
+
const rules: CursorRule[] = systemPrompt?.trim()
|
|
270
|
+
? [
|
|
271
|
+
create(CursorRuleSchema, {
|
|
272
|
+
fullPath: "/pi/system-prompt.mdc",
|
|
273
|
+
content: systemPrompt,
|
|
274
|
+
source: 2,
|
|
275
|
+
type: create(CursorRuleTypeSchema, {
|
|
276
|
+
type: {
|
|
277
|
+
case: "global",
|
|
278
|
+
value: create(CursorRuleTypeGlobalSchema, {}),
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
}),
|
|
282
|
+
]
|
|
283
|
+
: [];
|
|
284
|
+
rules.push(
|
|
285
|
+
create(CursorRuleSchema, {
|
|
286
|
+
fullPath: "/pi/cursor-chat-only.mdc",
|
|
287
|
+
content: CURSOR_CHAT_ONLY_SYSTEM_PROMPT,
|
|
288
|
+
source: 2,
|
|
289
|
+
type: create(CursorRuleTypeSchema, {
|
|
290
|
+
type: { case: "global", value: create(CursorRuleTypeGlobalSchema, {}) },
|
|
291
|
+
}),
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
return rules;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildHistoryTurns(
|
|
298
|
+
messages: Message[],
|
|
299
|
+
store: CursorBlobStore,
|
|
300
|
+
activeUserIndex: number,
|
|
301
|
+
): Uint8Array[] {
|
|
302
|
+
const turns: Uint8Array[] = [];
|
|
303
|
+
const end = activeUserIndex >= 0 ? activeUserIndex : messages.length;
|
|
304
|
+
let index = 0;
|
|
305
|
+
while (index < end) {
|
|
306
|
+
const user = messages[index];
|
|
307
|
+
if (user.role !== "user") {
|
|
308
|
+
index++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const userMessage = storeBlob(
|
|
312
|
+
store,
|
|
313
|
+
toBinary(UserMessageSchema, userMessageFromContent(user.content)),
|
|
314
|
+
);
|
|
315
|
+
const steps: Uint8Array[] = [];
|
|
316
|
+
index++;
|
|
317
|
+
while (index < end && messages[index]?.role !== "user") {
|
|
318
|
+
const message = messages[index];
|
|
319
|
+
if (message.role === "assistant") {
|
|
320
|
+
for (const item of message.content) {
|
|
321
|
+
if (item.type === "text" && item.text) {
|
|
322
|
+
steps.push(
|
|
323
|
+
storeBlob(
|
|
324
|
+
store,
|
|
325
|
+
toBinary(
|
|
326
|
+
ConversationStepSchema,
|
|
327
|
+
create(ConversationStepSchema, {
|
|
328
|
+
message: {
|
|
329
|
+
case: "assistantMessage",
|
|
330
|
+
value: create(AssistantMessageSchema, {
|
|
331
|
+
text: item.text,
|
|
332
|
+
}),
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
index++;
|
|
342
|
+
}
|
|
343
|
+
const turn = create(ConversationTurnStructureSchema, {
|
|
344
|
+
turn: {
|
|
345
|
+
case: "agentConversationTurn",
|
|
346
|
+
value: create(AgentConversationTurnStructureSchema, {
|
|
347
|
+
userMessage,
|
|
348
|
+
steps,
|
|
349
|
+
}),
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
turns.push(
|
|
353
|
+
storeBlob(store, toBinary(ConversationTurnStructureSchema, turn)),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return turns;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function lastUserIndex(messages: Message[]): number {
|
|
360
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
361
|
+
const role = messages[index]?.role;
|
|
362
|
+
if (role === "user") return index;
|
|
363
|
+
}
|
|
364
|
+
return -1;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
type CursorModelWithOptions = Model<Api> & { cursorMaxMode?: boolean };
|
|
368
|
+
|
|
369
|
+
function hasCursorMaxMode(model: Model<Api>): model is CursorModelWithOptions {
|
|
370
|
+
return Object.hasOwn(model, "cursorMaxMode");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function cursorMaxMode(model: Model<Api>): boolean {
|
|
374
|
+
return hasCursorMaxMode(model) && model.cursorMaxMode === true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function resolveWireModel(model: Model<Api>): {
|
|
378
|
+
modelId: string;
|
|
379
|
+
parameters: RequestedModel_ModelParameterbytes[];
|
|
380
|
+
} {
|
|
381
|
+
const id = model.id;
|
|
382
|
+
// Cursor resolves the bare Composer 2.5 id to its Fast lane unless the
|
|
383
|
+
// Standard tier is requested explicitly.
|
|
384
|
+
if (id === "composer-2.5") {
|
|
385
|
+
return {
|
|
386
|
+
modelId: id,
|
|
387
|
+
parameters: [
|
|
388
|
+
create(RequestedModel_ModelParameterbytesSchema, {
|
|
389
|
+
id: "fast",
|
|
390
|
+
value: "false",
|
|
391
|
+
}),
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const match = /^(.*)-(minimal|low|medium|high|xhigh|max)(-fast)?$/.exec(id);
|
|
396
|
+
if (!match?.[1] || !/(?:gpt|codex|o\d)/i.test(match[1])) {
|
|
397
|
+
return { modelId: id, parameters: [] };
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
modelId: `${match[1]}${match[3] ?? ""}`,
|
|
401
|
+
parameters: [
|
|
402
|
+
create(RequestedModel_ModelParameterbytesSchema, {
|
|
403
|
+
id: "reasoning",
|
|
404
|
+
value: match[2]!,
|
|
405
|
+
}),
|
|
406
|
+
],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Build the protobuf Run request and retain blobs for the same Connect stream. */
|
|
411
|
+
export async function buildCursorRequest(
|
|
412
|
+
model: Model<Api>,
|
|
413
|
+
context: Context,
|
|
414
|
+
options?: SimpleStreamOptions,
|
|
415
|
+
): Promise<CursorRequestBuild> {
|
|
416
|
+
const store: CursorBlobStore = new Map();
|
|
417
|
+
const activeIndex = lastUserIndex(context.messages);
|
|
418
|
+
const active = activeIndex >= 0 ? context.messages[activeIndex] : undefined;
|
|
419
|
+
const activeContent = active?.role === "user" ? active.content : undefined;
|
|
420
|
+
const rootPromptMessagesJson = [
|
|
421
|
+
...buildSystemPrompt(context.systemPrompt, store),
|
|
422
|
+
...buildHistoryRootPrompt(context.messages, store, activeIndex),
|
|
423
|
+
];
|
|
424
|
+
const state = create(ConversationStateStructureSchema, {
|
|
425
|
+
rootPromptMessagesJson,
|
|
426
|
+
turns: buildHistoryTurns(context.messages, store, activeIndex),
|
|
427
|
+
pendingToolCalls: [],
|
|
428
|
+
});
|
|
429
|
+
const conversationId = options?.sessionId ?? randomUUID();
|
|
430
|
+
const action = create(ConversationActionSchema, {
|
|
431
|
+
action:
|
|
432
|
+
activeContent !== undefined &&
|
|
433
|
+
(textFromContent(activeContent).length > 0 ||
|
|
434
|
+
imagesFromContent(activeContent).length > 0)
|
|
435
|
+
? {
|
|
436
|
+
case: "userMessageAction",
|
|
437
|
+
value: create(UserMessageActionSchema, {
|
|
438
|
+
userMessage: userMessageFromContent(activeContent),
|
|
439
|
+
}),
|
|
440
|
+
}
|
|
441
|
+
: { case: "resumeAction", value: create(ResumeActionSchema, {}) },
|
|
442
|
+
});
|
|
443
|
+
const wire = resolveWireModel(model);
|
|
444
|
+
let request = create(AgentRunRequestSchema, {
|
|
445
|
+
conversationState: state,
|
|
446
|
+
action,
|
|
447
|
+
modelDetails: create(ModelDetailsSchema, {
|
|
448
|
+
modelId: wire.modelId,
|
|
449
|
+
displayModelId: model.id,
|
|
450
|
+
displayName: model.name,
|
|
451
|
+
displayNameShort: model.name,
|
|
452
|
+
aliases: [],
|
|
453
|
+
...(cursorMaxMode(model) ? { maxMode: true } : {}),
|
|
454
|
+
}),
|
|
455
|
+
requestedModel: create(RequestedModelSchema, {
|
|
456
|
+
modelId: wire.modelId,
|
|
457
|
+
maxMode: cursorMaxMode(model),
|
|
458
|
+
parameters: wire.parameters,
|
|
459
|
+
}),
|
|
460
|
+
conversationId,
|
|
461
|
+
});
|
|
462
|
+
const replacement = await options?.onPayload?.(request, model);
|
|
463
|
+
if (replacement !== undefined) request = replacement as AgentRunRequest;
|
|
464
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
465
|
+
message: { case: "runRequest", value: request },
|
|
466
|
+
});
|
|
467
|
+
return {
|
|
468
|
+
request,
|
|
469
|
+
requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
|
|
470
|
+
blobStore: store,
|
|
471
|
+
conversationState: state,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function sanitizeCallerHeaders(
|
|
476
|
+
headers: SimpleStreamOptions["headers"],
|
|
477
|
+
): Record<string, string> {
|
|
478
|
+
const result: Record<string, string> = {};
|
|
479
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
480
|
+
if (value === null) continue;
|
|
481
|
+
const field = name.toLowerCase();
|
|
482
|
+
if (field.startsWith(":")) continue;
|
|
483
|
+
if (
|
|
484
|
+
HTTP2_FORBIDDEN_HEADERS.has(field) ||
|
|
485
|
+
CURSOR_RESERVED_HEADERS.has(field)
|
|
486
|
+
)
|
|
487
|
+
continue;
|
|
488
|
+
result[field] = value;
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function cursorHeaders(
|
|
494
|
+
apiKey: string,
|
|
495
|
+
options: SimpleStreamOptions | undefined,
|
|
496
|
+
): Record<string, string> {
|
|
497
|
+
return {
|
|
498
|
+
...sanitizeCallerHeaders(options?.headers),
|
|
499
|
+
":method": "POST",
|
|
500
|
+
":path": CURSOR_RUN_PATH,
|
|
501
|
+
"content-type": "application/connect+proto",
|
|
502
|
+
"connect-protocol-version": "1",
|
|
503
|
+
te: "trailers",
|
|
504
|
+
authorization: `Bearer ${apiKey}`,
|
|
505
|
+
"x-ghost-mode": "true",
|
|
506
|
+
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
507
|
+
"x-cursor-client-type": "cli",
|
|
508
|
+
"x-request-id": randomUUID(),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function headerRecord(
|
|
513
|
+
headers: http2.IncomingHttpHeaders,
|
|
514
|
+
): Record<string, string> {
|
|
515
|
+
const result: Record<string, string> = {};
|
|
516
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
517
|
+
if (typeof value === "string") result[key] = value;
|
|
518
|
+
else if (Array.isArray(value)) result[key] = value.join(", ");
|
|
519
|
+
}
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function errorFromEndStream(data: Uint8Array): Error | undefined {
|
|
524
|
+
try {
|
|
525
|
+
const parsed: unknown = JSON.parse(new TextDecoder().decode(data));
|
|
526
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
527
|
+
const error = parsed.error;
|
|
528
|
+
if (error && typeof error === "object") {
|
|
529
|
+
const message =
|
|
530
|
+
"message" in error && typeof error.message === "string"
|
|
531
|
+
? error.message
|
|
532
|
+
: "Cursor Connect error";
|
|
533
|
+
const code =
|
|
534
|
+
"code" in error && typeof error.code === "string"
|
|
535
|
+
? error.code
|
|
536
|
+
: "unknown";
|
|
537
|
+
return new Error(`Connect error ${code}: ${message}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return undefined;
|
|
541
|
+
} catch {
|
|
542
|
+
return new Error("Failed to parse Cursor Connect end-stream envelope");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function isAbortError(
|
|
547
|
+
error: unknown,
|
|
548
|
+
signal: AbortSignal | undefined,
|
|
549
|
+
): boolean {
|
|
550
|
+
return (
|
|
551
|
+
Boolean(signal?.aborted) ||
|
|
552
|
+
(error instanceof Error &&
|
|
553
|
+
/aborted|cancelled|canceled/i.test(error.message))
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Cursor AgentService/Run, deliberately chat-only (no context.tools advertisement or Pi tool calls). */
|
|
558
|
+
export function streamCursor(
|
|
559
|
+
model: Model<Api>,
|
|
560
|
+
context: Context,
|
|
561
|
+
options?: SimpleStreamOptions,
|
|
562
|
+
): AssistantMessageEventStream {
|
|
563
|
+
const stream = createAssistantMessageEventStream();
|
|
564
|
+
(async () => {
|
|
565
|
+
const output: AssistantMessage = {
|
|
566
|
+
role: "assistant",
|
|
567
|
+
content: [],
|
|
568
|
+
api: model.api,
|
|
569
|
+
provider: model.provider,
|
|
570
|
+
model: model.id,
|
|
571
|
+
usage: emptyUsage(),
|
|
572
|
+
stopReason: "pending",
|
|
573
|
+
timestamp: Date.now(),
|
|
574
|
+
};
|
|
575
|
+
let h2Client: http2.ClientHttp2Session | undefined;
|
|
576
|
+
let h2Request: http2.ClientHttp2Stream | undefined;
|
|
577
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
578
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
579
|
+
let removeAbortListener: (() => void) | undefined;
|
|
580
|
+
let currentText:
|
|
581
|
+
| Extract<AssistantMessage["content"][number], { type: "text" }>
|
|
582
|
+
| undefined;
|
|
583
|
+
let currentThinking:
|
|
584
|
+
| Extract<AssistantMessage["content"][number], { type: "thinking" }>
|
|
585
|
+
| undefined;
|
|
586
|
+
let turnEnded = false;
|
|
587
|
+
let terminalError: Error | undefined;
|
|
588
|
+
let finished = false;
|
|
589
|
+
|
|
590
|
+
const closeBlocks = () => {
|
|
591
|
+
if (currentText) {
|
|
592
|
+
const index = output.content.indexOf(currentText);
|
|
593
|
+
stream.push({
|
|
594
|
+
type: "text_end",
|
|
595
|
+
contentIndex: index,
|
|
596
|
+
content: currentText.text,
|
|
597
|
+
partial: output,
|
|
598
|
+
});
|
|
599
|
+
currentText = undefined;
|
|
600
|
+
}
|
|
601
|
+
if (currentThinking) {
|
|
602
|
+
const index = output.content.indexOf(currentThinking);
|
|
603
|
+
stream.push({
|
|
604
|
+
type: "thinking_end",
|
|
605
|
+
contentIndex: index,
|
|
606
|
+
content: currentThinking.thinking,
|
|
607
|
+
partial: output,
|
|
608
|
+
});
|
|
609
|
+
currentThinking = undefined;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const finishError = (error: unknown) => {
|
|
614
|
+
if (finished) return;
|
|
615
|
+
finished = true;
|
|
616
|
+
closeBlocks();
|
|
617
|
+
output.stopReason = isAbortError(error, options?.signal)
|
|
618
|
+
? "aborted"
|
|
619
|
+
: "error";
|
|
620
|
+
output.errorMessage =
|
|
621
|
+
error instanceof Error ? error.message : String(error);
|
|
622
|
+
stream.push({
|
|
623
|
+
type: "error",
|
|
624
|
+
reason: output.stopReason,
|
|
625
|
+
error: output,
|
|
626
|
+
});
|
|
627
|
+
stream.end();
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const apiKey = options?.apiKey?.trim();
|
|
632
|
+
if (!apiKey)
|
|
633
|
+
throw new Error("Cursor API key is required — run /login cursor");
|
|
634
|
+
if (options?.fetch) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
"Cursor uses an HTTP/2 transport and does not support options.fetch",
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (options?.signal?.aborted) throw new Error("Cursor request aborted");
|
|
640
|
+
const timeoutMs = options?.timeoutMs;
|
|
641
|
+
if (
|
|
642
|
+
timeoutMs !== undefined &&
|
|
643
|
+
(!Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
644
|
+
) {
|
|
645
|
+
throw new Error(`Invalid timeoutMs: ${String(timeoutMs)}`);
|
|
646
|
+
}
|
|
647
|
+
const requestTimeoutMs =
|
|
648
|
+
timeoutMs === undefined || timeoutMs === 0
|
|
649
|
+
? undefined
|
|
650
|
+
: Math.max(1, Math.floor(timeoutMs));
|
|
651
|
+
const built = await buildCursorRequest(model, context, options);
|
|
652
|
+
const baseUrl = model.baseUrl || CURSOR_API_URL;
|
|
653
|
+
const completion = Promise.withResolvers<void>();
|
|
654
|
+
let completionSettled = false;
|
|
655
|
+
const settle = (error?: unknown) => {
|
|
656
|
+
if (completionSettled) return;
|
|
657
|
+
completionSettled = true;
|
|
658
|
+
if (error !== undefined) completion.reject(error);
|
|
659
|
+
else if (terminalError) completion.reject(terminalError);
|
|
660
|
+
else if (!turnEnded)
|
|
661
|
+
completion.reject(new Error("Cursor stream ended before turnEnded"));
|
|
662
|
+
else completion.resolve();
|
|
663
|
+
};
|
|
664
|
+
// Abort can reject completion while we are still awaiting response
|
|
665
|
+
// headers; keep the rejection observed so Node does not report it as
|
|
666
|
+
// unhandled when the catch path never reaches `await completion.promise`.
|
|
667
|
+
void completion.promise.catch(() => {});
|
|
668
|
+
const responseReady = Promise.withResolvers<void>();
|
|
669
|
+
let responseSeen = false;
|
|
670
|
+
let responseStatus = 0;
|
|
671
|
+
let responseHeaders: Record<string, string> = {};
|
|
672
|
+
let responseReadySettled = false;
|
|
673
|
+
const rejectResponseReady = (error: unknown) => {
|
|
674
|
+
if (responseReadySettled) return;
|
|
675
|
+
responseReadySettled = true;
|
|
676
|
+
responseReady.reject(error);
|
|
677
|
+
};
|
|
678
|
+
const resolveResponseReady = () => {
|
|
679
|
+
if (responseReadySettled) return;
|
|
680
|
+
responseReadySettled = true;
|
|
681
|
+
responseReady.resolve();
|
|
682
|
+
};
|
|
683
|
+
const clearIdleTimer = () => {
|
|
684
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
685
|
+
idleTimer = undefined;
|
|
686
|
+
};
|
|
687
|
+
const armIdleTimer = () => {
|
|
688
|
+
clearIdleTimer();
|
|
689
|
+
if (requestTimeoutMs === undefined) return;
|
|
690
|
+
idleTimer = setTimeout(() => {
|
|
691
|
+
const error = new Error(
|
|
692
|
+
`Cursor request idle timeout after ${requestTimeoutMs}ms`,
|
|
693
|
+
);
|
|
694
|
+
rejectResponseReady(error);
|
|
695
|
+
settle(error);
|
|
696
|
+
h2Request?.close(http2.constants.NGHTTP2_CANCEL);
|
|
697
|
+
}, requestTimeoutMs);
|
|
698
|
+
};
|
|
699
|
+
let frameBuffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
700
|
+
const processFrame = (flags: number, bytes: Uint8Array) => {
|
|
701
|
+
if ((flags & CONNECT_COMPRESSED_FLAG) !== 0) {
|
|
702
|
+
throw new Error("Compressed Cursor Connect frames are unsupported");
|
|
703
|
+
}
|
|
704
|
+
if ((flags & CONNECT_END_STREAM_FLAG) !== 0) {
|
|
705
|
+
terminalError = errorFromEndStream(bytes);
|
|
706
|
+
if (terminalError) h2Request?.close();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const message = fromBinary(AgentServerMessageSchema, bytes);
|
|
710
|
+
if (message.message.case === "execServerMessage") {
|
|
711
|
+
const exec = message.message.value;
|
|
712
|
+
if (exec.message.case === "requestContextArgs") {
|
|
713
|
+
const result = create(RequestContextResultSchema, {
|
|
714
|
+
result: {
|
|
715
|
+
case: "success",
|
|
716
|
+
value: create(RequestContextSuccessSchema, {
|
|
717
|
+
requestContext: create(RequestContextSchema, {
|
|
718
|
+
rules: buildCursorRequestContextRules(context.systemPrompt),
|
|
719
|
+
tools: [],
|
|
720
|
+
}),
|
|
721
|
+
}),
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
const response = create(ExecClientMessageSchema, {
|
|
725
|
+
id: exec.id,
|
|
726
|
+
execId: exec.execId,
|
|
727
|
+
message: { case: "requestContextResult", value: result },
|
|
728
|
+
});
|
|
729
|
+
const envelope = create(AgentClientMessageSchema, {
|
|
730
|
+
message: { case: "execClientMessage", value: response },
|
|
731
|
+
});
|
|
732
|
+
h2Request?.write(
|
|
733
|
+
frameConnectMessage(toBinary(AgentClientMessageSchema, envelope)),
|
|
734
|
+
);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const throwReply = create(AgentClientMessageSchema, {
|
|
738
|
+
message: {
|
|
739
|
+
case: "execClientControlMessage",
|
|
740
|
+
value: create(ExecClientControlMessageSchema, {
|
|
741
|
+
message: {
|
|
742
|
+
case: "throw",
|
|
743
|
+
value: create(ExecClientThrowSchema, {
|
|
744
|
+
id: exec.id,
|
|
745
|
+
error:
|
|
746
|
+
"Cursor tools are not available in this chat-only provider",
|
|
747
|
+
errorCode: "UNIMPLEMENTED",
|
|
748
|
+
}),
|
|
749
|
+
},
|
|
750
|
+
}),
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
const closeReply = create(AgentClientMessageSchema, {
|
|
754
|
+
message: {
|
|
755
|
+
case: "execClientControlMessage",
|
|
756
|
+
value: create(ExecClientControlMessageSchema, {
|
|
757
|
+
message: {
|
|
758
|
+
case: "streamClose",
|
|
759
|
+
value: create(ExecClientStreamCloseSchema, { id: exec.id }),
|
|
760
|
+
},
|
|
761
|
+
}),
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
const error = new Error(
|
|
765
|
+
"Cursor requested a tool that is unavailable in chat-only mode",
|
|
766
|
+
);
|
|
767
|
+
terminalError = error;
|
|
768
|
+
if (!h2Request) {
|
|
769
|
+
settle(error);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
h2Request.write(
|
|
773
|
+
frameConnectMessage(toBinary(AgentClientMessageSchema, throwReply)),
|
|
774
|
+
);
|
|
775
|
+
h2Request.write(
|
|
776
|
+
frameConnectMessage(toBinary(AgentClientMessageSchema, closeReply)),
|
|
777
|
+
() => settle(error),
|
|
778
|
+
);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (message.message.case === "kvServerMessage") {
|
|
782
|
+
sendKvReply(message.message.value, built.blobStore, h2Request);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (message.message.case === "interactionQuery") {
|
|
786
|
+
throw new Error(
|
|
787
|
+
`Cursor interaction query ${message.message.value.query.case ?? "unknown"} is unavailable in chat-only mode`,
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (message.message.case !== "interactionUpdate") return;
|
|
791
|
+
processInteraction(
|
|
792
|
+
message.message.value,
|
|
793
|
+
output,
|
|
794
|
+
stream,
|
|
795
|
+
() => {
|
|
796
|
+
turnEnded = true;
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
setText(value) {
|
|
800
|
+
currentText = value;
|
|
801
|
+
},
|
|
802
|
+
getText() {
|
|
803
|
+
return currentText;
|
|
804
|
+
},
|
|
805
|
+
setThinking(value) {
|
|
806
|
+
currentThinking = value;
|
|
807
|
+
},
|
|
808
|
+
getThinking() {
|
|
809
|
+
return currentThinking;
|
|
810
|
+
},
|
|
811
|
+
closeBlocks,
|
|
812
|
+
},
|
|
813
|
+
);
|
|
814
|
+
};
|
|
815
|
+
const processData = (chunk: Buffer) => {
|
|
816
|
+
frameBuffer =
|
|
817
|
+
frameBuffer.length === 0
|
|
818
|
+
? chunk
|
|
819
|
+
: Buffer.concat([frameBuffer, chunk]);
|
|
820
|
+
while (frameBuffer.length >= 5) {
|
|
821
|
+
const size = frameBuffer.readUInt32BE(1);
|
|
822
|
+
if (size > MAX_CONNECT_FRAME_BYTES) {
|
|
823
|
+
throw new Error(
|
|
824
|
+
`Cursor Connect frame exceeds ${MAX_CONNECT_FRAME_BYTES} bytes`,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
if (frameBuffer.length < size + 5) return;
|
|
828
|
+
const flags = frameBuffer[0]!;
|
|
829
|
+
const data = frameBuffer.subarray(5, size + 5);
|
|
830
|
+
frameBuffer = frameBuffer.subarray(size + 5);
|
|
831
|
+
processFrame(flags, data);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
h2Client = await connectCursorHttp2(baseUrl, {
|
|
836
|
+
signal: options?.signal,
|
|
837
|
+
timeoutMs: Math.min(
|
|
838
|
+
requestTimeoutMs ?? PROXY_TUNNEL_TIMEOUT_MS,
|
|
839
|
+
PROXY_TUNNEL_TIMEOUT_MS,
|
|
840
|
+
),
|
|
841
|
+
});
|
|
842
|
+
h2Client.once("error", (error) => {
|
|
843
|
+
rejectResponseReady(error);
|
|
844
|
+
settle(error);
|
|
845
|
+
});
|
|
846
|
+
h2Request = h2Client.request(cursorHeaders(apiKey, options));
|
|
847
|
+
h2Request.once("response", (headers) => {
|
|
848
|
+
armIdleTimer();
|
|
849
|
+
responseSeen = true;
|
|
850
|
+
responseStatus = Number(headers[":status"] ?? 0);
|
|
851
|
+
responseHeaders = headerRecord(headers);
|
|
852
|
+
resolveResponseReady();
|
|
853
|
+
});
|
|
854
|
+
h2Request.on("trailers", (trailers) => {
|
|
855
|
+
const status = String(trailers["grpc-status"] ?? "0");
|
|
856
|
+
if (status !== "0") {
|
|
857
|
+
const encodedMessage = String(trailers["grpc-message"] ?? "");
|
|
858
|
+
try {
|
|
859
|
+
terminalError = new Error(
|
|
860
|
+
`Cursor gRPC error ${status}: ${decodeURIComponent(encodedMessage)}`,
|
|
861
|
+
);
|
|
862
|
+
} catch (cause) {
|
|
863
|
+
const error = new Error(
|
|
864
|
+
`Cursor gRPC error ${status} contains a malformed grpc-message trailer`,
|
|
865
|
+
{ cause },
|
|
866
|
+
);
|
|
867
|
+
terminalError = error;
|
|
868
|
+
settle(error);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
const responseCallback = responseReady.promise.then(async () => {
|
|
873
|
+
await options?.onResponse?.(
|
|
874
|
+
{ status: responseStatus, headers: responseHeaders },
|
|
875
|
+
model,
|
|
876
|
+
);
|
|
877
|
+
if (responseStatus < 200 || responseStatus >= 300) {
|
|
878
|
+
throw new Error(
|
|
879
|
+
`Cursor AgentService request failed with HTTP ${responseStatus}`,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
stream.push({ type: "start", partial: output });
|
|
883
|
+
});
|
|
884
|
+
// Keep rejected transport/callback promises observed even when the peer
|
|
885
|
+
// closes immediately after a malformed or unsupported interaction.
|
|
886
|
+
void responseReady.promise.catch(() => {});
|
|
887
|
+
void responseCallback.catch(() => {});
|
|
888
|
+
let dataChain = Promise.resolve();
|
|
889
|
+
h2Request.on("data", (chunk: Buffer) => {
|
|
890
|
+
armIdleTimer();
|
|
891
|
+
dataChain = dataChain
|
|
892
|
+
.then(() => responseCallback)
|
|
893
|
+
.then(() => processData(chunk))
|
|
894
|
+
.catch((error) => {
|
|
895
|
+
settle(error);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
h2Request.once("end", () => {
|
|
899
|
+
clearIdleTimer();
|
|
900
|
+
if (!responseSeen) {
|
|
901
|
+
rejectResponseReady(
|
|
902
|
+
new Error("Cursor response headers were not received"),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
void dataChain
|
|
906
|
+
.then(() => responseCallback)
|
|
907
|
+
.then(() => {
|
|
908
|
+
if (!responseSeen)
|
|
909
|
+
throw new Error("Cursor response headers were not received");
|
|
910
|
+
if (frameBuffer.length !== 0)
|
|
911
|
+
throw new Error("Incomplete Cursor Connect frame");
|
|
912
|
+
settle();
|
|
913
|
+
})
|
|
914
|
+
.catch((error) => settle(error));
|
|
915
|
+
});
|
|
916
|
+
h2Request.once("error", (error) => {
|
|
917
|
+
rejectResponseReady(error);
|
|
918
|
+
settle(error);
|
|
919
|
+
});
|
|
920
|
+
h2Request.once("aborted", () => {
|
|
921
|
+
const error = new Error("Cursor response aborted");
|
|
922
|
+
rejectResponseReady(error);
|
|
923
|
+
settle(error);
|
|
924
|
+
});
|
|
925
|
+
const sendHeartbeat = () => {
|
|
926
|
+
if (!h2Request || h2Request.closed || h2Request.destroyed) return;
|
|
927
|
+
const message = create(AgentClientMessageSchema, {
|
|
928
|
+
message: {
|
|
929
|
+
case: "clientHeartbeat",
|
|
930
|
+
value: create(ClientHeartbeatSchema, {}),
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
try {
|
|
934
|
+
h2Request.write(
|
|
935
|
+
frameConnectMessage(toBinary(AgentClientMessageSchema, message)),
|
|
936
|
+
);
|
|
937
|
+
} catch {
|
|
938
|
+
// The terminal request/error handler owns stream completion.
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
heartbeat = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
|
|
942
|
+
if (options?.signal) {
|
|
943
|
+
const onAbort = () => {
|
|
944
|
+
const error = new Error("Cursor request aborted");
|
|
945
|
+
rejectResponseReady(error);
|
|
946
|
+
h2Request?.close(http2.constants.NGHTTP2_CANCEL);
|
|
947
|
+
settle(error);
|
|
948
|
+
};
|
|
949
|
+
if (options.signal.aborted) onAbort();
|
|
950
|
+
else {
|
|
951
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
952
|
+
removeAbortListener = () =>
|
|
953
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
armIdleTimer();
|
|
957
|
+
h2Request.write(frameConnectMessage(built.requestBytes));
|
|
958
|
+
await responseCallback;
|
|
959
|
+
await completion.promise;
|
|
960
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
961
|
+
clearIdleTimer();
|
|
962
|
+
removeAbortListener?.();
|
|
963
|
+
h2Request.close();
|
|
964
|
+
h2Client.close();
|
|
965
|
+
closeBlocks();
|
|
966
|
+
if (output.stopReason === "pending") output.stopReason = "stop";
|
|
967
|
+
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
968
|
+
stream.push({
|
|
969
|
+
type: "done",
|
|
970
|
+
reason: output.stopReason === "length" ? "length" : "stop",
|
|
971
|
+
message: output,
|
|
972
|
+
});
|
|
973
|
+
stream.end();
|
|
974
|
+
} catch (error) {
|
|
975
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
976
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
977
|
+
removeAbortListener?.();
|
|
978
|
+
h2Request?.close();
|
|
979
|
+
h2Client?.close();
|
|
980
|
+
finishError(error);
|
|
981
|
+
}
|
|
982
|
+
})().catch((error) => {
|
|
983
|
+
// The body above handles all expected failures; this guard also protects
|
|
984
|
+
// the event stream from an unexpected asynchronous callback rejection.
|
|
985
|
+
stream.push({
|
|
986
|
+
type: "error",
|
|
987
|
+
reason: "error",
|
|
988
|
+
error: {
|
|
989
|
+
role: "assistant",
|
|
990
|
+
content: [],
|
|
991
|
+
api: model.api,
|
|
992
|
+
provider: model.provider,
|
|
993
|
+
model: model.id,
|
|
994
|
+
usage: emptyUsage(),
|
|
995
|
+
stopReason: "error",
|
|
996
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
997
|
+
timestamp: Date.now(),
|
|
998
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
stream.end();
|
|
1001
|
+
});
|
|
1002
|
+
return stream;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
interface InteractionState {
|
|
1006
|
+
setText(
|
|
1007
|
+
value:
|
|
1008
|
+
| Extract<AssistantMessage["content"][number], { type: "text" }>
|
|
1009
|
+
| undefined,
|
|
1010
|
+
): void;
|
|
1011
|
+
getText():
|
|
1012
|
+
| Extract<AssistantMessage["content"][number], { type: "text" }>
|
|
1013
|
+
| undefined;
|
|
1014
|
+
setThinking(
|
|
1015
|
+
value:
|
|
1016
|
+
| Extract<AssistantMessage["content"][number], { type: "thinking" }>
|
|
1017
|
+
| undefined,
|
|
1018
|
+
): void;
|
|
1019
|
+
getThinking():
|
|
1020
|
+
| Extract<AssistantMessage["content"][number], { type: "thinking" }>
|
|
1021
|
+
| undefined;
|
|
1022
|
+
closeBlocks(): void;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function processInteraction(
|
|
1026
|
+
update: InteractionUpdate,
|
|
1027
|
+
output: AssistantMessage,
|
|
1028
|
+
stream: AssistantMessageEventStream,
|
|
1029
|
+
onTurnEnded: () => void,
|
|
1030
|
+
state: InteractionState,
|
|
1031
|
+
): void {
|
|
1032
|
+
switch (update.message.case) {
|
|
1033
|
+
case "textDelta": {
|
|
1034
|
+
const thinking = state.getThinking();
|
|
1035
|
+
if (thinking) {
|
|
1036
|
+
const index = output.content.indexOf(thinking);
|
|
1037
|
+
stream.push({
|
|
1038
|
+
type: "thinking_end",
|
|
1039
|
+
contentIndex: index,
|
|
1040
|
+
content: thinking.thinking,
|
|
1041
|
+
partial: output,
|
|
1042
|
+
});
|
|
1043
|
+
state.setThinking(undefined);
|
|
1044
|
+
}
|
|
1045
|
+
const delta = update.message.value.text;
|
|
1046
|
+
if (!delta) return;
|
|
1047
|
+
let block = state.getText();
|
|
1048
|
+
if (!block) {
|
|
1049
|
+
block = { type: "text", text: "" };
|
|
1050
|
+
output.content.push(block);
|
|
1051
|
+
state.setText(block);
|
|
1052
|
+
stream.push({
|
|
1053
|
+
type: "text_start",
|
|
1054
|
+
contentIndex: output.content.length - 1,
|
|
1055
|
+
partial: output,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
block.text += delta;
|
|
1059
|
+
stream.push({
|
|
1060
|
+
type: "text_delta",
|
|
1061
|
+
contentIndex: output.content.indexOf(block),
|
|
1062
|
+
delta,
|
|
1063
|
+
partial: output,
|
|
1064
|
+
});
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case "thinkingDelta": {
|
|
1068
|
+
const delta = update.message.value.text;
|
|
1069
|
+
if (!delta) return;
|
|
1070
|
+
const text = state.getText();
|
|
1071
|
+
if (text) {
|
|
1072
|
+
const index = output.content.indexOf(text);
|
|
1073
|
+
stream.push({
|
|
1074
|
+
type: "text_end",
|
|
1075
|
+
contentIndex: index,
|
|
1076
|
+
content: text.text,
|
|
1077
|
+
partial: output,
|
|
1078
|
+
});
|
|
1079
|
+
state.setText(undefined);
|
|
1080
|
+
}
|
|
1081
|
+
let block = state.getThinking();
|
|
1082
|
+
if (!block) {
|
|
1083
|
+
block = { type: "thinking", thinking: "" };
|
|
1084
|
+
output.content.push(block);
|
|
1085
|
+
state.setThinking(block);
|
|
1086
|
+
stream.push({
|
|
1087
|
+
type: "thinking_start",
|
|
1088
|
+
contentIndex: output.content.length - 1,
|
|
1089
|
+
partial: output,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
block.thinking += delta;
|
|
1093
|
+
stream.push({
|
|
1094
|
+
type: "thinking_delta",
|
|
1095
|
+
contentIndex: output.content.indexOf(block),
|
|
1096
|
+
delta,
|
|
1097
|
+
partial: output,
|
|
1098
|
+
});
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
case "thinkingCompleted": {
|
|
1102
|
+
const block = state.getThinking();
|
|
1103
|
+
if (!block) return;
|
|
1104
|
+
const index = output.content.indexOf(block);
|
|
1105
|
+
stream.push({
|
|
1106
|
+
type: "thinking_end",
|
|
1107
|
+
contentIndex: index,
|
|
1108
|
+
content: block.thinking,
|
|
1109
|
+
partial: output,
|
|
1110
|
+
});
|
|
1111
|
+
state.setThinking(undefined);
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
case "partialToolCall":
|
|
1115
|
+
case "toolCallDelta":
|
|
1116
|
+
case "toolCallStarted":
|
|
1117
|
+
case "toolCallCompleted":
|
|
1118
|
+
throw new Error(
|
|
1119
|
+
`Cursor ${update.message.case} is unavailable in chat-only mode`,
|
|
1120
|
+
);
|
|
1121
|
+
case "tokenDelta": {
|
|
1122
|
+
// Cursor only reports generated tokens here, not the complete context
|
|
1123
|
+
// usage Pi needs for context accounting. Keep the usage block empty;
|
|
1124
|
+
// Pi 0.84.3+ estimates the full history for threshold compaction.
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
case "turnEnded":
|
|
1128
|
+
onTurnEnded();
|
|
1129
|
+
break;
|
|
1130
|
+
case "heartbeat":
|
|
1131
|
+
case undefined:
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function sendKvReply(
|
|
1137
|
+
message: KvServerMessage,
|
|
1138
|
+
store: CursorBlobStore,
|
|
1139
|
+
request: http2.ClientHttp2Stream | undefined,
|
|
1140
|
+
): void {
|
|
1141
|
+
if (!request || request.closed || request.destroyed) return;
|
|
1142
|
+
let reply;
|
|
1143
|
+
if (message.message.case === "getBlobArgs") {
|
|
1144
|
+
const key = Buffer.from(message.message.value.blobId).toString("hex");
|
|
1145
|
+
reply = create(KvClientMessageSchema, {
|
|
1146
|
+
id: message.id,
|
|
1147
|
+
message: {
|
|
1148
|
+
case: "getBlobResult",
|
|
1149
|
+
value: create(GetBlobResultSchema, { blobData: store.get(key) }),
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
} else if (message.message.case === "setBlobArgs") {
|
|
1153
|
+
const args = message.message.value;
|
|
1154
|
+
store.set(Buffer.from(args.blobId).toString("hex"), args.blobData);
|
|
1155
|
+
reply = create(KvClientMessageSchema, {
|
|
1156
|
+
id: message.id,
|
|
1157
|
+
message: {
|
|
1158
|
+
case: "setBlobResult",
|
|
1159
|
+
value: create(SetBlobResultSchema, {}),
|
|
1160
|
+
},
|
|
1161
|
+
});
|
|
1162
|
+
} else {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const envelope = create(AgentClientMessageSchema, {
|
|
1166
|
+
message: { case: "kvClientMessage", value: reply },
|
|
1167
|
+
});
|
|
1168
|
+
try {
|
|
1169
|
+
request.write(
|
|
1170
|
+
frameConnectMessage(toBinary(AgentClientMessageSchema, envelope)),
|
|
1171
|
+
);
|
|
1172
|
+
} catch {
|
|
1173
|
+
// The owning stream listener reports the transport failure.
|
|
1174
|
+
}
|
|
1175
|
+
}
|