@playwo/opencode-cursor-oauth 0.0.0-dev.12f4642b8f6b → 0.0.0-dev.14c6316643ec
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/dist/cursor/bidi-session.d.ts +1 -2
- package/dist/cursor/bidi-session.js +153 -138
- package/dist/cursor/index.d.ts +1 -1
- package/dist/cursor/index.js +1 -1
- package/dist/cursor/unary-rpc.d.ts +0 -1
- package/dist/cursor/unary-rpc.js +2 -59
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +3 -0
- package/dist/openai/messages.js +5 -0
- package/dist/plugin/cursor-auth-plugin.js +0 -1
- package/dist/proxy/bridge-close-controller.d.ts +6 -0
- package/dist/proxy/bridge-close-controller.js +37 -0
- package/dist/proxy/bridge-non-streaming.js +18 -2
- package/dist/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +129 -18
- package/dist/proxy/chat-completion.js +43 -3
- package/dist/proxy/cursor-request.d.ts +1 -0
- package/dist/proxy/cursor-request.js +23 -8
- package/dist/proxy/server.js +23 -5
- package/dist/proxy/stream-dispatch.d.ts +6 -1
- package/dist/proxy/stream-dispatch.js +241 -11
- package/dist/proxy/types.d.ts +6 -1
- package/package.json +1 -2
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
2
|
import { AgentClientMessageSchema, AgentServerMessageSchema, ExecClientMessageSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolResultContentItemSchema, } from "../proto/agent_pb";
|
|
3
|
-
import { errorDetails, logPluginError, logPluginWarn } from "../logger";
|
|
3
|
+
import { errorDetails, logPluginError, logPluginInfo, logPluginWarn, } from "../logger";
|
|
4
4
|
import { formatToolCallSummary, formatToolResultSummary, } from "../openai/messages";
|
|
5
5
|
import { activeBridges, updateStoredConversationAfterCompletion, } from "./conversation-state";
|
|
6
6
|
import { startBridge } from "./bridge-session";
|
|
7
7
|
import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
|
|
8
8
|
import { SSE_HEADERS } from "./sse";
|
|
9
9
|
import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
|
|
10
|
+
import { createBridgeCloseController } from "./bridge-close-controller";
|
|
10
11
|
const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
11
|
-
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
12
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
12
13
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
13
14
|
const created = Math.floor(Date.now() / 1000);
|
|
14
15
|
let keepaliveTimer;
|
|
16
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
15
17
|
const stopKeepalive = () => {
|
|
16
18
|
if (!keepaliveTimer)
|
|
17
19
|
return;
|
|
@@ -47,6 +49,19 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
47
49
|
return;
|
|
48
50
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
49
51
|
};
|
|
52
|
+
const failStream = (message, code) => {
|
|
53
|
+
if (closed)
|
|
54
|
+
return;
|
|
55
|
+
sendSSE({
|
|
56
|
+
error: {
|
|
57
|
+
message,
|
|
58
|
+
type: "server_error",
|
|
59
|
+
...(code ? { code } : {}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
sendDone();
|
|
63
|
+
closeController();
|
|
64
|
+
};
|
|
50
65
|
const closeController = () => {
|
|
51
66
|
if (closed)
|
|
52
67
|
return;
|
|
@@ -75,7 +90,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
75
90
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
76
91
|
try {
|
|
77
92
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
78
|
-
processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
93
|
+
processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
79
94
|
if (isThinking) {
|
|
80
95
|
sendSSE(makeChunk({ reasoning_content: text }));
|
|
81
96
|
return;
|
|
@@ -88,7 +103,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
88
103
|
sendSSE(makeChunk({ content }));
|
|
89
104
|
}
|
|
90
105
|
}, (exec) => {
|
|
91
|
-
state.pendingExecs.
|
|
106
|
+
const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
|
|
107
|
+
if (existingIndex >= 0) {
|
|
108
|
+
state.pendingExecs[existingIndex] = exec;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
state.pendingExecs.push(exec);
|
|
112
|
+
}
|
|
92
113
|
mcpExecReceived = true;
|
|
93
114
|
const flushed = tagFilter.flush();
|
|
94
115
|
if (flushed.reasoning)
|
|
@@ -127,6 +148,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
127
148
|
bridge,
|
|
128
149
|
heartbeatTimer,
|
|
129
150
|
blobStore,
|
|
151
|
+
cloudRule,
|
|
130
152
|
mcpTools,
|
|
131
153
|
pendingExecs: state.pendingExecs,
|
|
132
154
|
modelId,
|
|
@@ -138,7 +160,21 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
138
160
|
sendSSE(makeChunk({}, "tool_calls"));
|
|
139
161
|
sendDone();
|
|
140
162
|
closeController();
|
|
141
|
-
}, (checkpointBytes) =>
|
|
163
|
+
}, (checkpointBytes) => {
|
|
164
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
165
|
+
bridgeCloseController.noteCheckpoint();
|
|
166
|
+
}, () => bridgeCloseController.noteTurnEnded(), (info) => {
|
|
167
|
+
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
168
|
+
logPluginError("Closing Cursor bridge after unsupported message", {
|
|
169
|
+
modelId,
|
|
170
|
+
bridgeKey,
|
|
171
|
+
convKey,
|
|
172
|
+
category: info.category,
|
|
173
|
+
caseName: info.caseName,
|
|
174
|
+
detail: info.detail,
|
|
175
|
+
});
|
|
176
|
+
scheduleBridgeEnd(bridge);
|
|
177
|
+
}, (info) => {
|
|
142
178
|
endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
|
|
143
179
|
logPluginError("Closing Cursor bridge after unsupported exec", {
|
|
144
180
|
modelId,
|
|
@@ -180,17 +216,30 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
180
216
|
stopKeepalive();
|
|
181
217
|
}
|
|
182
218
|
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
219
|
+
logPluginInfo("Opened Cursor streaming bridge", {
|
|
220
|
+
modelId,
|
|
221
|
+
bridgeKey,
|
|
222
|
+
convKey,
|
|
223
|
+
mcpToolCount: mcpTools.length,
|
|
224
|
+
hasCloudRule: Boolean(cloudRule),
|
|
225
|
+
});
|
|
183
226
|
bridge.onData(processChunk);
|
|
184
227
|
bridge.onClose((code) => {
|
|
228
|
+
logPluginInfo("Cursor streaming bridge closed", {
|
|
229
|
+
modelId,
|
|
230
|
+
bridgeKey,
|
|
231
|
+
convKey,
|
|
232
|
+
code,
|
|
233
|
+
mcpExecReceived,
|
|
234
|
+
hadEndStreamError: Boolean(endStreamError),
|
|
235
|
+
});
|
|
236
|
+
bridgeCloseController.dispose();
|
|
185
237
|
clearInterval(heartbeatTimer);
|
|
186
238
|
stopKeepalive();
|
|
187
239
|
syncStoredBlobStore(convKey, blobStore);
|
|
188
240
|
if (endStreamError) {
|
|
189
241
|
activeBridges.delete(bridgeKey);
|
|
190
|
-
|
|
191
|
-
closed = true;
|
|
192
|
-
controller.error(endStreamError);
|
|
193
|
-
}
|
|
242
|
+
failStream(endStreamError.message, "cursor_bridge_closed");
|
|
194
243
|
return;
|
|
195
244
|
}
|
|
196
245
|
if (!mcpExecReceived) {
|
|
@@ -210,15 +259,12 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
210
259
|
}
|
|
211
260
|
activeBridges.delete(bridgeKey);
|
|
212
261
|
if (code !== 0 && !closed) {
|
|
213
|
-
|
|
214
|
-
sendSSE(makeChunk({}, "stop"));
|
|
215
|
-
sendSSE(makeUsageChunk());
|
|
216
|
-
sendDone();
|
|
217
|
-
closeController();
|
|
262
|
+
failStream("Cursor bridge connection lost", "cursor_bridge_closed");
|
|
218
263
|
}
|
|
219
264
|
});
|
|
220
265
|
},
|
|
221
266
|
cancel(reason) {
|
|
267
|
+
bridgeCloseController.dispose();
|
|
222
268
|
stopKeepalive();
|
|
223
269
|
clearInterval(heartbeatTimer);
|
|
224
270
|
syncStoredBlobStore(convKey, blobStore);
|
|
@@ -238,11 +284,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
238
284
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
239
285
|
}
|
|
240
286
|
export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
287
|
+
logPluginInfo("Starting Cursor streaming response", {
|
|
288
|
+
modelId,
|
|
289
|
+
bridgeKey,
|
|
290
|
+
convKey,
|
|
291
|
+
mcpToolCount: payload.mcpTools.length,
|
|
292
|
+
});
|
|
241
293
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
242
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
294
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
243
295
|
}
|
|
244
|
-
|
|
245
|
-
const
|
|
296
|
+
async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
|
|
297
|
+
const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
298
|
+
const deadline = Date.now() + timeoutMs;
|
|
299
|
+
while (Date.now() < deadline) {
|
|
300
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
301
|
+
if (unresolved.length === 0) {
|
|
302
|
+
return unresolved;
|
|
303
|
+
}
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
305
|
+
}
|
|
306
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
307
|
+
if (unresolved.length > 0) {
|
|
308
|
+
logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
|
|
309
|
+
bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
|
|
310
|
+
modelId: active.modelId,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return unresolved;
|
|
314
|
+
}
|
|
315
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
316
|
+
const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
246
317
|
const resumeMetadata = {
|
|
247
318
|
...metadata,
|
|
248
319
|
assistantSeedText: [
|
|
@@ -252,6 +323,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
252
323
|
.filter(Boolean)
|
|
253
324
|
.join("\n\n"),
|
|
254
325
|
};
|
|
326
|
+
logPluginInfo("Preparing Cursor tool-result resume", {
|
|
327
|
+
bridgeKey,
|
|
328
|
+
convKey,
|
|
329
|
+
modelId,
|
|
330
|
+
toolResults,
|
|
331
|
+
pendingExecs,
|
|
332
|
+
});
|
|
333
|
+
const unresolved = await waitForResolvablePendingExecs(active, toolResults);
|
|
334
|
+
logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
|
|
335
|
+
bridgeKey,
|
|
336
|
+
convKey,
|
|
337
|
+
modelId,
|
|
338
|
+
toolResults,
|
|
339
|
+
pendingExecs,
|
|
340
|
+
unresolvedPendingExecs: unresolved,
|
|
341
|
+
});
|
|
342
|
+
if (unresolved.length > 0) {
|
|
343
|
+
clearInterval(heartbeatTimer);
|
|
344
|
+
bridge.end();
|
|
345
|
+
return new Response(JSON.stringify({
|
|
346
|
+
error: {
|
|
347
|
+
message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
|
|
348
|
+
type: "invalid_request_error",
|
|
349
|
+
code: "cursor_missing_exec_metadata",
|
|
350
|
+
},
|
|
351
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
352
|
+
}
|
|
255
353
|
for (const exec of pendingExecs) {
|
|
256
354
|
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
257
355
|
const mcpResult = result
|
|
@@ -292,7 +390,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
292
390
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
293
391
|
message: { case: "execClientMessage", value: execClientMessage },
|
|
294
392
|
});
|
|
393
|
+
logPluginInfo("Sending Cursor tool-result resume message", {
|
|
394
|
+
bridgeKey,
|
|
395
|
+
convKey,
|
|
396
|
+
modelId,
|
|
397
|
+
toolCallId: exec.toolCallId,
|
|
398
|
+
toolName: exec.toolName,
|
|
399
|
+
source: exec.source,
|
|
400
|
+
execId: exec.execId,
|
|
401
|
+
execMsgId: exec.execMsgId,
|
|
402
|
+
cursorCallId: exec.cursorCallId,
|
|
403
|
+
modelCallId: exec.modelCallId,
|
|
404
|
+
matchedToolResult: result,
|
|
405
|
+
});
|
|
295
406
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
296
407
|
}
|
|
297
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
408
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
298
409
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logPluginWarn } from "../logger";
|
|
1
|
+
import { logPluginInfo, logPluginWarn } from "../logger";
|
|
2
2
|
import { buildInitialHandoffPrompt, buildTitleSourceText, buildToolResumePrompt, detectTitleRequest, parseMessages, } from "../openai/messages";
|
|
3
3
|
import { buildMcpToolDefinitions, selectToolsForChoice } from "../openai/tools";
|
|
4
4
|
import { activeBridges, conversationStates, createStoredConversation, deriveBridgeKey, deriveConversationKey, evictStaleConversations, hashString, normalizeAgentKey, resetStoredConversation, } from "./conversation-state";
|
|
@@ -10,6 +10,19 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
10
10
|
const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
|
|
11
11
|
const modelId = body.model;
|
|
12
12
|
const normalizedAgentKey = normalizeAgentKey(context.agentKey);
|
|
13
|
+
logPluginInfo("Handling Cursor chat completion request", {
|
|
14
|
+
modelId,
|
|
15
|
+
stream: body.stream !== false,
|
|
16
|
+
messageCount: body.messages.length,
|
|
17
|
+
toolCount: body.tools?.length ?? 0,
|
|
18
|
+
toolChoice: body.tool_choice,
|
|
19
|
+
sessionId: context.sessionId,
|
|
20
|
+
agentKey: normalizedAgentKey,
|
|
21
|
+
parsedUserText: userText,
|
|
22
|
+
parsedToolResults: toolResults,
|
|
23
|
+
hasPendingAssistantSummary: pendingAssistantSummary.trim().length > 0,
|
|
24
|
+
turnCount: turns.length,
|
|
25
|
+
});
|
|
13
26
|
const titleDetection = detectTitleRequest(body);
|
|
14
27
|
const isTitleAgent = titleDetection.matched;
|
|
15
28
|
if (isTitleAgent) {
|
|
@@ -38,7 +51,23 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
38
51
|
const bridgeKey = deriveBridgeKey(modelId, body.messages, context.sessionId, context.agentKey);
|
|
39
52
|
const convKey = deriveConversationKey(body.messages, context.sessionId, context.agentKey);
|
|
40
53
|
const activeBridge = activeBridges.get(bridgeKey);
|
|
54
|
+
logPluginInfo("Resolved Cursor conversation keys", {
|
|
55
|
+
modelId,
|
|
56
|
+
bridgeKey,
|
|
57
|
+
convKey,
|
|
58
|
+
hasActiveBridge: Boolean(activeBridge),
|
|
59
|
+
sessionId: context.sessionId,
|
|
60
|
+
agentKey: normalizedAgentKey,
|
|
61
|
+
});
|
|
41
62
|
if (activeBridge && toolResults.length > 0) {
|
|
63
|
+
logPluginInfo("Matched OpenAI tool results to active Cursor bridge", {
|
|
64
|
+
bridgeKey,
|
|
65
|
+
convKey,
|
|
66
|
+
requestedModelId: modelId,
|
|
67
|
+
activeBridgeModelId: activeBridge.modelId,
|
|
68
|
+
toolResults,
|
|
69
|
+
pendingExecs: activeBridge.pendingExecs,
|
|
70
|
+
});
|
|
42
71
|
activeBridges.delete(bridgeKey);
|
|
43
72
|
if (activeBridge.bridge.alive) {
|
|
44
73
|
if (activeBridge.modelId !== modelId) {
|
|
@@ -82,16 +111,27 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
82
111
|
// Build the request. When tool results are present but the bridge died,
|
|
83
112
|
// we must still include the last user text so Cursor has context.
|
|
84
113
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
114
|
+
const hasPendingAssistantSummary = pendingAssistantSummary.trim().length > 0;
|
|
85
115
|
const needsInitialHandoff = !stored.checkpoint &&
|
|
86
|
-
(turns.length > 0 ||
|
|
116
|
+
(turns.length > 0 || hasPendingAssistantSummary || toolResults.length > 0);
|
|
87
117
|
const replayTurns = needsInitialHandoff ? [] : turns;
|
|
88
118
|
let effectiveUserText = needsInitialHandoff
|
|
89
119
|
? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
|
|
90
|
-
: toolResults.length > 0
|
|
120
|
+
: toolResults.length > 0 || hasPendingAssistantSummary
|
|
91
121
|
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
92
122
|
: userText;
|
|
93
123
|
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
94
124
|
payload.mcpTools = mcpTools;
|
|
125
|
+
logPluginInfo("Built Cursor run request payload", {
|
|
126
|
+
modelId,
|
|
127
|
+
bridgeKey,
|
|
128
|
+
convKey,
|
|
129
|
+
mcpToolCount: mcpTools.length,
|
|
130
|
+
conversationId: stored.conversationId,
|
|
131
|
+
hasCheckpoint: Boolean(stored.checkpoint),
|
|
132
|
+
replayTurnCount: replayTurns.length,
|
|
133
|
+
effectiveUserText,
|
|
134
|
+
});
|
|
95
135
|
if (body.stream === false) {
|
|
96
136
|
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
|
|
97
137
|
systemPrompt,
|
|
@@ -3,3 +3,4 @@ export declare function buildCursorRequest(modelId: string, systemPrompt: string
|
|
|
3
3
|
userText: string;
|
|
4
4
|
assistantText: string;
|
|
5
5
|
}>, conversationId: string, checkpoint: Uint8Array | null, existingBlobStore?: Map<string, Uint8Array>): CursorRequestPayload;
|
|
6
|
+
export declare function buildCursorResumeRequest(modelId: string, systemPrompt: string, conversationId: string, checkpoint: Uint8Array, existingBlobStore?: Map<string, Uint8Array>): CursorRequestPayload;
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
|
-
import {
|
|
3
|
-
import { AgentClientMessageSchema, AgentRunRequestSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, ModelDetailsSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
|
|
2
|
+
import { AgentClientMessageSchema, AgentRunRequestSchema, AgentConversationTurnStructureSchema, AssistantMessageSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationTurnStructureSchema, ConversationStepSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
|
|
4
3
|
export function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) {
|
|
5
4
|
const blobStore = new Map(existingBlobStore ?? []);
|
|
6
|
-
|
|
7
|
-
const systemJson = JSON.stringify({ role: "system", content: systemPrompt });
|
|
8
|
-
const systemBytes = new TextEncoder().encode(systemJson);
|
|
9
|
-
const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest());
|
|
10
|
-
blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes);
|
|
5
|
+
const cloudRule = buildCloudRule(systemPrompt);
|
|
11
6
|
let conversationState;
|
|
12
7
|
if (checkpoint) {
|
|
13
8
|
conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
|
|
@@ -40,7 +35,7 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
|
|
|
40
35
|
turnBytes.push(toBinary(ConversationTurnStructureSchema, turnStructure));
|
|
41
36
|
}
|
|
42
37
|
conversationState = create(ConversationStateStructureSchema, {
|
|
43
|
-
rootPromptMessagesJson: [
|
|
38
|
+
rootPromptMessagesJson: [],
|
|
44
39
|
turns: turnBytes,
|
|
45
40
|
todos: [],
|
|
46
41
|
pendingToolCalls: [],
|
|
@@ -64,6 +59,21 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
|
|
|
64
59
|
value: create(UserMessageActionSchema, { userMessage }),
|
|
65
60
|
},
|
|
66
61
|
});
|
|
62
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
|
|
63
|
+
}
|
|
64
|
+
export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
|
|
65
|
+
const blobStore = new Map(existingBlobStore ?? []);
|
|
66
|
+
const cloudRule = buildCloudRule(systemPrompt);
|
|
67
|
+
const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
|
|
68
|
+
const action = create(ConversationActionSchema, {
|
|
69
|
+
action: {
|
|
70
|
+
case: "resumeAction",
|
|
71
|
+
value: create(ResumeActionSchema, {}),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
|
|
75
|
+
}
|
|
76
|
+
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule) {
|
|
67
77
|
const modelDetails = create(ModelDetailsSchema, {
|
|
68
78
|
modelId,
|
|
69
79
|
displayModelId: modelId,
|
|
@@ -81,6 +91,11 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
|
|
|
81
91
|
return {
|
|
82
92
|
requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
|
|
83
93
|
blobStore,
|
|
94
|
+
cloudRule,
|
|
84
95
|
mcpTools: [],
|
|
85
96
|
};
|
|
86
97
|
}
|
|
98
|
+
function buildCloudRule(systemPrompt) {
|
|
99
|
+
const content = systemPrompt.trim();
|
|
100
|
+
return content || undefined;
|
|
101
|
+
}
|
package/dist/proxy/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorDetails, logPluginError } from "../logger";
|
|
1
|
+
import { errorDetails, logPluginError, logPluginWarn } from "../logger";
|
|
2
2
|
import { handleChatCompletion } from "./chat-completion";
|
|
3
3
|
import { activeBridges, conversationStates } from "./conversation-state";
|
|
4
4
|
let proxyServer;
|
|
@@ -42,14 +42,32 @@ export async function startProxy(getAccessToken, models = []) {
|
|
|
42
42
|
throw new Error("Cursor proxy access token provider not configured");
|
|
43
43
|
}
|
|
44
44
|
const accessToken = await proxyAccessTokenProvider();
|
|
45
|
-
const sessionId = req.headers.get("x-
|
|
46
|
-
req.headers.get("x-session-id") ??
|
|
47
|
-
undefined;
|
|
45
|
+
const sessionId = req.headers.get("x-session-id") ?? undefined;
|
|
48
46
|
const agentKey = req.headers.get("x-opencode-agent") ?? undefined;
|
|
49
|
-
|
|
47
|
+
const response = await handleChatCompletion(body, accessToken, {
|
|
50
48
|
sessionId,
|
|
51
49
|
agentKey,
|
|
52
50
|
});
|
|
51
|
+
if (response.status >= 400) {
|
|
52
|
+
let responseBody = "";
|
|
53
|
+
try {
|
|
54
|
+
responseBody = await response.clone().text();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
responseBody = `Failed to read rejected response body: ${error instanceof Error ? error.message : String(error)}`;
|
|
58
|
+
}
|
|
59
|
+
logPluginWarn("Rejected Cursor chat completion", {
|
|
60
|
+
path: url.pathname,
|
|
61
|
+
method: req.method,
|
|
62
|
+
sessionId,
|
|
63
|
+
agentKey,
|
|
64
|
+
status: response.status,
|
|
65
|
+
requestBody: body,
|
|
66
|
+
requestBodyText: JSON.stringify(body),
|
|
67
|
+
responseBody,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return response;
|
|
53
71
|
}
|
|
54
72
|
catch (err) {
|
|
55
73
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7,6 +7,11 @@ export interface UnhandledExecInfo {
|
|
|
7
7
|
execId: string;
|
|
8
8
|
execMsgId: number;
|
|
9
9
|
}
|
|
10
|
+
export interface UnsupportedServerMessageInfo {
|
|
11
|
+
category: "agentMessage" | "interactionUpdate" | "interactionQuery" | "execServerControl" | "toolCall";
|
|
12
|
+
caseName: string;
|
|
13
|
+
detail?: string;
|
|
14
|
+
}
|
|
10
15
|
export declare function parseConnectEndStream(data: Uint8Array): Error | null;
|
|
11
16
|
export declare function makeHeartbeatBytes(): Uint8Array;
|
|
12
17
|
export declare function scheduleBridgeEnd(bridge: CursorSession): void;
|
|
@@ -34,4 +39,4 @@ export declare function computeUsage(state: StreamState): {
|
|
|
34
39
|
completion_tokens: number;
|
|
35
40
|
total_tokens: number;
|
|
36
41
|
};
|
|
37
|
-
export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
|
|
42
|
+
export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, cloudRule: string | undefined, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
|