@playwo/opencode-cursor-oauth 0.0.0-dev.d7836f7ad39f → 0.0.0-dev.da5538092563
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 +33 -12
- 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.d.ts +0 -1
- package/dist/openai/messages.js +0 -3
- 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 +8 -4
- package/dist/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +118 -20
- package/dist/proxy/chat-completion.js +45 -30
- package/dist/proxy/cursor-request.js +12 -15
- package/dist/proxy/server.js +23 -5
- package/dist/proxy/stream-dispatch.d.ts +1 -1
- package/dist/proxy/stream-dispatch.js +210 -60
- package/dist/proxy/stream-state.d.ts +0 -2
- package/dist/proxy/types.d.ts +6 -1
- package/package.json +1 -2
|
@@ -2,4 +2,4 @@ import { type ToolResultInfo } from "../openai/messages";
|
|
|
2
2
|
import type { ConversationRequestMetadata } from "./conversation-meta";
|
|
3
3
|
import type { ActiveBridge, CursorRequestPayload } from "./types";
|
|
4
4
|
export declare function handleStreamingResponse(payload: CursorRequestPayload, accessToken: string, modelId: string, bridgeKey: string, convKey: string, metadata: ConversationRequestMetadata): Promise<Response>;
|
|
5
|
-
export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Response
|
|
5
|
+
export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Promise<Response>;
|
|
@@ -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;
|
|
@@ -27,8 +29,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
27
29
|
pendingExecs: [],
|
|
28
30
|
outputTokens: 0,
|
|
29
31
|
totalTokens: 0,
|
|
30
|
-
interactionToolArgsText: new Map(),
|
|
31
|
-
emittedToolCallIds: new Set(),
|
|
32
32
|
};
|
|
33
33
|
const tagFilter = createThinkingTagFilter();
|
|
34
34
|
let assistantText = metadata.assistantSeedText ?? "";
|
|
@@ -49,6 +49,19 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
49
49
|
return;
|
|
50
50
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
51
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
|
+
};
|
|
52
65
|
const closeController = () => {
|
|
53
66
|
if (closed)
|
|
54
67
|
return;
|
|
@@ -77,7 +90,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
77
90
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
78
91
|
try {
|
|
79
92
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
80
|
-
processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
93
|
+
processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
81
94
|
if (isThinking) {
|
|
82
95
|
sendSSE(makeChunk({ reasoning_content: text }));
|
|
83
96
|
return;
|
|
@@ -90,7 +103,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
90
103
|
sendSSE(makeChunk({ content }));
|
|
91
104
|
}
|
|
92
105
|
}, (exec) => {
|
|
93
|
-
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
|
+
}
|
|
94
113
|
mcpExecReceived = true;
|
|
95
114
|
const flushed = tagFilter.flush();
|
|
96
115
|
if (flushed.reasoning)
|
|
@@ -129,6 +148,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
129
148
|
bridge,
|
|
130
149
|
heartbeatTimer,
|
|
131
150
|
blobStore,
|
|
151
|
+
cloudRule,
|
|
132
152
|
mcpTools,
|
|
133
153
|
pendingExecs: state.pendingExecs,
|
|
134
154
|
modelId,
|
|
@@ -140,7 +160,10 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
140
160
|
sendSSE(makeChunk({}, "tool_calls"));
|
|
141
161
|
sendDone();
|
|
142
162
|
closeController();
|
|
143
|
-
}, (checkpointBytes) =>
|
|
163
|
+
}, (checkpointBytes) => {
|
|
164
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
165
|
+
bridgeCloseController.noteCheckpoint();
|
|
166
|
+
}, () => bridgeCloseController.noteTurnEnded(), (info) => {
|
|
144
167
|
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
145
168
|
logPluginError("Closing Cursor bridge after unsupported message", {
|
|
146
169
|
modelId,
|
|
@@ -193,17 +216,30 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
193
216
|
stopKeepalive();
|
|
194
217
|
}
|
|
195
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
|
+
});
|
|
196
226
|
bridge.onData(processChunk);
|
|
197
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();
|
|
198
237
|
clearInterval(heartbeatTimer);
|
|
199
238
|
stopKeepalive();
|
|
200
239
|
syncStoredBlobStore(convKey, blobStore);
|
|
201
240
|
if (endStreamError) {
|
|
202
241
|
activeBridges.delete(bridgeKey);
|
|
203
|
-
|
|
204
|
-
closed = true;
|
|
205
|
-
controller.error(endStreamError);
|
|
206
|
-
}
|
|
242
|
+
failStream(endStreamError.message, "cursor_bridge_closed");
|
|
207
243
|
return;
|
|
208
244
|
}
|
|
209
245
|
if (!mcpExecReceived) {
|
|
@@ -223,15 +259,12 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
223
259
|
}
|
|
224
260
|
activeBridges.delete(bridgeKey);
|
|
225
261
|
if (code !== 0 && !closed) {
|
|
226
|
-
|
|
227
|
-
sendSSE(makeChunk({}, "stop"));
|
|
228
|
-
sendSSE(makeUsageChunk());
|
|
229
|
-
sendDone();
|
|
230
|
-
closeController();
|
|
262
|
+
failStream("Cursor bridge connection lost", "cursor_bridge_closed");
|
|
231
263
|
}
|
|
232
264
|
});
|
|
233
265
|
},
|
|
234
266
|
cancel(reason) {
|
|
267
|
+
bridgeCloseController.dispose();
|
|
235
268
|
stopKeepalive();
|
|
236
269
|
clearInterval(heartbeatTimer);
|
|
237
270
|
syncStoredBlobStore(convKey, blobStore);
|
|
@@ -251,11 +284,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
251
284
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
252
285
|
}
|
|
253
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
|
+
});
|
|
254
293
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
255
|
-
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);
|
|
295
|
+
}
|
|
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;
|
|
256
314
|
}
|
|
257
|
-
export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
258
|
-
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
315
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
316
|
+
const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
259
317
|
const resumeMetadata = {
|
|
260
318
|
...metadata,
|
|
261
319
|
assistantSeedText: [
|
|
@@ -265,6 +323,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
265
323
|
.filter(Boolean)
|
|
266
324
|
.join("\n\n"),
|
|
267
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
|
+
}
|
|
268
353
|
for (const exec of pendingExecs) {
|
|
269
354
|
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
270
355
|
const mcpResult = result
|
|
@@ -305,7 +390,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
305
390
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
306
391
|
message: { case: "execClientMessage", value: execClientMessage },
|
|
307
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
|
+
});
|
|
308
406
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
309
407
|
}
|
|
310
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
408
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
311
409
|
}
|
|
@@ -1,15 +1,28 @@
|
|
|
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";
|
|
5
|
-
import { buildCursorRequest
|
|
5
|
+
import { buildCursorRequest } from "./cursor-request";
|
|
6
6
|
import { handleNonStreamingResponse, handleStreamingResponse, handleToolResultResume, } from "./bridge";
|
|
7
7
|
import { handleTitleGenerationRequest } from "./title";
|
|
8
8
|
export function handleChatCompletion(body, accessToken, context = {}) {
|
|
9
9
|
const parsed = parseMessages(body.messages);
|
|
10
|
-
const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint,
|
|
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) {
|
|
@@ -79,44 +108,30 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
79
108
|
stored.completedTurnsFingerprint = completedTurnsFingerprint;
|
|
80
109
|
stored.lastAccessMs = Date.now();
|
|
81
110
|
evictStaleConversations();
|
|
82
|
-
if (assistantContinuation) {
|
|
83
|
-
if (!stored.checkpoint) {
|
|
84
|
-
return new Response(JSON.stringify({
|
|
85
|
-
error: {
|
|
86
|
-
message: "Assistant-last continuation requires an existing Cursor checkpoint",
|
|
87
|
-
type: "invalid_request_error",
|
|
88
|
-
},
|
|
89
|
-
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
90
|
-
}
|
|
91
|
-
const payload = buildCursorResumeRequest(modelId, systemPrompt, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
92
|
-
payload.mcpTools = buildMcpToolDefinitions(tools);
|
|
93
|
-
const metadata = {
|
|
94
|
-
systemPrompt,
|
|
95
|
-
systemPromptHash,
|
|
96
|
-
completedTurnsFingerprint,
|
|
97
|
-
turns,
|
|
98
|
-
userText,
|
|
99
|
-
assistantSeedText: pendingAssistantSummary,
|
|
100
|
-
agentKey: normalizedAgentKey,
|
|
101
|
-
};
|
|
102
|
-
if (body.stream === false) {
|
|
103
|
-
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata);
|
|
104
|
-
}
|
|
105
|
-
return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata);
|
|
106
|
-
}
|
|
107
111
|
// Build the request. When tool results are present but the bridge died,
|
|
108
112
|
// we must still include the last user text so Cursor has context.
|
|
109
113
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
114
|
+
const hasPendingAssistantSummary = pendingAssistantSummary.trim().length > 0;
|
|
110
115
|
const needsInitialHandoff = !stored.checkpoint &&
|
|
111
|
-
(turns.length > 0 ||
|
|
116
|
+
(turns.length > 0 || hasPendingAssistantSummary || toolResults.length > 0);
|
|
112
117
|
const replayTurns = needsInitialHandoff ? [] : turns;
|
|
113
118
|
let effectiveUserText = needsInitialHandoff
|
|
114
119
|
? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
|
|
115
|
-
: toolResults.length > 0
|
|
120
|
+
: toolResults.length > 0 || hasPendingAssistantSummary
|
|
116
121
|
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
117
122
|
: userText;
|
|
118
123
|
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
119
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
|
+
});
|
|
120
135
|
if (body.stream === false) {
|
|
121
136
|
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
|
|
122
137
|
systemPrompt,
|
|
@@ -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, ResumeActionSchema, 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,14 +59,11 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
|
|
|
64
59
|
value: create(UserMessageActionSchema, { userMessage }),
|
|
65
60
|
},
|
|
66
61
|
});
|
|
67
|
-
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
|
|
62
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
|
|
68
63
|
}
|
|
69
64
|
export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
|
|
70
65
|
const blobStore = new Map(existingBlobStore ?? []);
|
|
71
|
-
const
|
|
72
|
-
const systemBytes = new TextEncoder().encode(systemJson);
|
|
73
|
-
const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest());
|
|
74
|
-
blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes);
|
|
66
|
+
const cloudRule = buildCloudRule(systemPrompt);
|
|
75
67
|
const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
|
|
76
68
|
const action = create(ConversationActionSchema, {
|
|
77
69
|
action: {
|
|
@@ -79,9 +71,9 @@ export function buildCursorResumeRequest(modelId, systemPrompt, conversationId,
|
|
|
79
71
|
value: create(ResumeActionSchema, {}),
|
|
80
72
|
},
|
|
81
73
|
});
|
|
82
|
-
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
|
|
74
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
|
|
83
75
|
}
|
|
84
|
-
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
|
|
76
|
+
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule) {
|
|
85
77
|
const modelDetails = create(ModelDetailsSchema, {
|
|
86
78
|
modelId,
|
|
87
79
|
displayModelId: modelId,
|
|
@@ -99,6 +91,11 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
|
|
|
99
91
|
return {
|
|
100
92
|
requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
|
|
101
93
|
blobStore,
|
|
94
|
+
cloudRule,
|
|
102
95
|
mcpTools: [],
|
|
103
96
|
};
|
|
104
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);
|
|
@@ -39,4 +39,4 @@ export declare function computeUsage(state: StreamState): {
|
|
|
39
39
|
completion_tokens: number;
|
|
40
40
|
total_tokens: number;
|
|
41
41
|
};
|
|
42
|
-
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, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => 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;
|