@playwo/opencode-cursor-oauth 0.0.0-dev.2c48be2f48c9 → 0.0.0-dev.36e1216df6cf
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/AGENTS.md +8 -0
- package/dist/agent-rules.d.ts +1 -0
- package/dist/agent-rules.js +28 -0
- 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 +33 -3
- package/dist/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +430 -64
- package/dist/proxy/chat-completion.js +44 -3
- package/dist/proxy/cursor-request.d.ts +1 -0
- package/dist/proxy/cursor-request.js +24 -8
- package/dist/proxy/server.js +23 -5
- package/dist/proxy/stream-dispatch.d.ts +17 -1
- package/dist/proxy/stream-dispatch.js +280 -12
- package/dist/proxy/types.d.ts +14 -1
- package/package.json +2 -3
|
@@ -1,17 +1,22 @@
|
|
|
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
|
|
12
|
+
function sortedIds(values) {
|
|
13
|
+
return [...values].sort();
|
|
14
|
+
}
|
|
15
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
12
16
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
13
17
|
const created = Math.floor(Date.now() / 1000);
|
|
14
18
|
let keepaliveTimer;
|
|
19
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
15
20
|
const stopKeepalive = () => {
|
|
16
21
|
if (!keepaliveTimer)
|
|
17
22
|
return;
|
|
@@ -32,6 +37,8 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
32
37
|
let assistantText = metadata.assistantSeedText ?? "";
|
|
33
38
|
let mcpExecReceived = false;
|
|
34
39
|
let endStreamError = null;
|
|
40
|
+
let toolCallsFlushed = false;
|
|
41
|
+
const streamedToolCalls = new Map();
|
|
35
42
|
const sendSSE = (data) => {
|
|
36
43
|
if (closed)
|
|
37
44
|
return;
|
|
@@ -47,6 +54,19 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
47
54
|
return;
|
|
48
55
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
49
56
|
};
|
|
57
|
+
const failStream = (message, code) => {
|
|
58
|
+
if (closed)
|
|
59
|
+
return;
|
|
60
|
+
sendSSE({
|
|
61
|
+
error: {
|
|
62
|
+
message,
|
|
63
|
+
type: "server_error",
|
|
64
|
+
...(code ? { code } : {}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
sendDone();
|
|
68
|
+
closeController();
|
|
69
|
+
};
|
|
50
70
|
const closeController = () => {
|
|
51
71
|
if (closed)
|
|
52
72
|
return;
|
|
@@ -72,10 +92,145 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
72
92
|
usage: { prompt_tokens, completion_tokens, total_tokens },
|
|
73
93
|
};
|
|
74
94
|
};
|
|
95
|
+
const ensureStreamedToolCall = (toolCallId, toolName) => {
|
|
96
|
+
const existing = streamedToolCalls.get(toolCallId);
|
|
97
|
+
if (existing) {
|
|
98
|
+
if (toolName && existing.toolName !== toolName) {
|
|
99
|
+
existing.toolName = toolName;
|
|
100
|
+
}
|
|
101
|
+
return existing;
|
|
102
|
+
}
|
|
103
|
+
const createdState = {
|
|
104
|
+
index: state.toolCallIndex++,
|
|
105
|
+
toolName,
|
|
106
|
+
started: false,
|
|
107
|
+
};
|
|
108
|
+
streamedToolCalls.set(toolCallId, createdState);
|
|
109
|
+
return createdState;
|
|
110
|
+
};
|
|
111
|
+
const emitPendingToolStart = (toolCallId, toolName, source) => {
|
|
112
|
+
if (!toolName)
|
|
113
|
+
return;
|
|
114
|
+
const toolCall = ensureStreamedToolCall(toolCallId, toolName);
|
|
115
|
+
if (toolCall.started)
|
|
116
|
+
return;
|
|
117
|
+
toolCall.started = true;
|
|
118
|
+
logPluginInfo("Streaming pending Cursor tool-call start", {
|
|
119
|
+
modelId,
|
|
120
|
+
bridgeKey,
|
|
121
|
+
convKey,
|
|
122
|
+
toolCallId,
|
|
123
|
+
toolName,
|
|
124
|
+
index: toolCall.index,
|
|
125
|
+
source,
|
|
126
|
+
});
|
|
127
|
+
sendSSE(makeChunk({
|
|
128
|
+
tool_calls: [
|
|
129
|
+
{
|
|
130
|
+
index: toolCall.index,
|
|
131
|
+
id: toolCallId,
|
|
132
|
+
type: "function",
|
|
133
|
+
function: {
|
|
134
|
+
name: toolCall.toolName,
|
|
135
|
+
arguments: "",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
}));
|
|
140
|
+
};
|
|
141
|
+
const publishPendingToolCalls = (source) => {
|
|
142
|
+
if (closed || toolCallsFlushed || state.pendingExecs.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
logPluginInfo("Evaluating Cursor tool-call publish", {
|
|
145
|
+
modelId,
|
|
146
|
+
bridgeKey,
|
|
147
|
+
convKey,
|
|
148
|
+
source,
|
|
149
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
150
|
+
toolCallsFlushed,
|
|
151
|
+
mcpExecReceived,
|
|
152
|
+
nowMs: Date.now(),
|
|
153
|
+
});
|
|
154
|
+
toolCallsFlushed = true;
|
|
155
|
+
const flushed = tagFilter.flush();
|
|
156
|
+
if (flushed.reasoning)
|
|
157
|
+
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
158
|
+
if (flushed.content) {
|
|
159
|
+
assistantText += flushed.content;
|
|
160
|
+
sendSSE(makeChunk({ content: flushed.content }));
|
|
161
|
+
}
|
|
162
|
+
const assistantSeedText = [
|
|
163
|
+
assistantText.trim(),
|
|
164
|
+
state.pendingExecs
|
|
165
|
+
.map((exec) => formatToolCallSummary({
|
|
166
|
+
id: exec.toolCallId,
|
|
167
|
+
type: "function",
|
|
168
|
+
function: {
|
|
169
|
+
name: exec.toolName,
|
|
170
|
+
arguments: exec.decodedArgs,
|
|
171
|
+
},
|
|
172
|
+
}))
|
|
173
|
+
.join("\n\n"),
|
|
174
|
+
]
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join("\n\n");
|
|
177
|
+
activeBridges.set(bridgeKey, {
|
|
178
|
+
bridge,
|
|
179
|
+
heartbeatTimer,
|
|
180
|
+
blobStore,
|
|
181
|
+
cloudRule,
|
|
182
|
+
mcpTools,
|
|
183
|
+
pendingExecs: [...state.pendingExecs],
|
|
184
|
+
modelId,
|
|
185
|
+
metadata: {
|
|
186
|
+
...metadata,
|
|
187
|
+
assistantSeedText,
|
|
188
|
+
},
|
|
189
|
+
diagnostics: {
|
|
190
|
+
announcedToolCallIds: [],
|
|
191
|
+
publishedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
|
|
192
|
+
lastMcpUpdate: "publish",
|
|
193
|
+
publishedAtMs: Date.now(),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
logPluginInfo("Publishing Cursor tool-call envelope", {
|
|
197
|
+
modelId,
|
|
198
|
+
bridgeKey,
|
|
199
|
+
convKey,
|
|
200
|
+
source,
|
|
201
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
202
|
+
emittedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
|
|
203
|
+
assistantSeedTextLength: assistantSeedText.length,
|
|
204
|
+
});
|
|
205
|
+
for (const exec of state.pendingExecs) {
|
|
206
|
+
emitPendingToolStart(exec.toolCallId, exec.toolName, "publish");
|
|
207
|
+
const streamedToolCall = ensureStreamedToolCall(exec.toolCallId, exec.toolName);
|
|
208
|
+
sendSSE(makeChunk({
|
|
209
|
+
tool_calls: [
|
|
210
|
+
{
|
|
211
|
+
index: streamedToolCall.index,
|
|
212
|
+
function: {
|
|
213
|
+
arguments: exec.decodedArgs,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
logPluginInfo("Stored active Cursor bridge for tool-result resume", {
|
|
220
|
+
modelId,
|
|
221
|
+
bridgeKey,
|
|
222
|
+
convKey,
|
|
223
|
+
diagnostics: activeBridges.get(bridgeKey)?.diagnostics,
|
|
224
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
225
|
+
});
|
|
226
|
+
sendSSE(makeChunk({}, "tool_calls"));
|
|
227
|
+
sendDone();
|
|
228
|
+
closeController();
|
|
229
|
+
};
|
|
75
230
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
76
231
|
try {
|
|
77
232
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
78
|
-
processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
233
|
+
processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
79
234
|
if (isThinking) {
|
|
80
235
|
sendSSE(makeChunk({ reasoning_content: text }));
|
|
81
236
|
return;
|
|
@@ -88,57 +243,143 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
88
243
|
sendSSE(makeChunk({ content }));
|
|
89
244
|
}
|
|
90
245
|
}, (exec) => {
|
|
91
|
-
|
|
246
|
+
if (toolCallsFlushed) {
|
|
247
|
+
logPluginWarn("Received Cursor MCP exec after tool-call envelope was already published", {
|
|
248
|
+
modelId,
|
|
249
|
+
bridgeKey,
|
|
250
|
+
convKey,
|
|
251
|
+
toolCallId: exec.toolCallId,
|
|
252
|
+
toolName: exec.toolName,
|
|
253
|
+
publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
|
|
254
|
+
[],
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
emitPendingToolStart(exec.toolCallId, exec.toolName, "exec");
|
|
258
|
+
const pendingBefore = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
|
|
259
|
+
const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
|
|
260
|
+
if (existingIndex >= 0) {
|
|
261
|
+
state.pendingExecs[existingIndex] = exec;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
state.pendingExecs.push(exec);
|
|
265
|
+
}
|
|
92
266
|
mcpExecReceived = true;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
267
|
+
const pendingAfter = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
|
|
268
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
269
|
+
if (existingActiveBridge) {
|
|
270
|
+
existingActiveBridge.diagnostics = {
|
|
271
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
272
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
273
|
+
lastMcpUpdate: `exec:${exec.toolCallId}`,
|
|
274
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
275
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
276
|
+
};
|
|
99
277
|
}
|
|
100
|
-
|
|
101
|
-
assistantText.trim(),
|
|
102
|
-
formatToolCallSummary({
|
|
103
|
-
id: exec.toolCallId,
|
|
104
|
-
type: "function",
|
|
105
|
-
function: {
|
|
106
|
-
name: exec.toolName,
|
|
107
|
-
arguments: exec.decodedArgs,
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
]
|
|
111
|
-
.filter(Boolean)
|
|
112
|
-
.join("\n\n");
|
|
113
|
-
sendSSE(makeChunk({
|
|
114
|
-
tool_calls: [
|
|
115
|
-
{
|
|
116
|
-
index: state.toolCallIndex++,
|
|
117
|
-
id: exec.toolCallId,
|
|
118
|
-
type: "function",
|
|
119
|
-
function: {
|
|
120
|
-
name: exec.toolName,
|
|
121
|
-
arguments: exec.decodedArgs,
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
}));
|
|
126
|
-
activeBridges.set(bridgeKey, {
|
|
127
|
-
bridge,
|
|
128
|
-
heartbeatTimer,
|
|
129
|
-
blobStore,
|
|
130
|
-
mcpTools,
|
|
131
|
-
pendingExecs: state.pendingExecs,
|
|
278
|
+
logPluginInfo("Tracking Cursor MCP exec in streaming bridge", {
|
|
132
279
|
modelId,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
280
|
+
bridgeKey,
|
|
281
|
+
convKey,
|
|
282
|
+
toolCallId: exec.toolCallId,
|
|
283
|
+
toolName: exec.toolName,
|
|
284
|
+
pendingBefore,
|
|
285
|
+
pendingAfter,
|
|
286
|
+
hasStoredActiveBridge: Boolean(existingActiveBridge),
|
|
287
|
+
storedActiveBridgePendingExecToolCallIds: existingActiveBridge
|
|
288
|
+
? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
|
|
289
|
+
: [],
|
|
290
|
+
});
|
|
291
|
+
}, (info) => {
|
|
292
|
+
if (toolCallsFlushed) {
|
|
293
|
+
logPluginWarn("Received Cursor MCP interaction update after tool-call envelope was already published", {
|
|
294
|
+
modelId,
|
|
295
|
+
bridgeKey,
|
|
296
|
+
convKey,
|
|
297
|
+
updateCase: info.updateCase,
|
|
298
|
+
toolCallId: info.toolCallId,
|
|
299
|
+
publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
|
|
300
|
+
[],
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (info.updateCase !== "toolCallCompleted") {
|
|
304
|
+
emitPendingToolStart(info.toolCallId, info.toolName ?? "", "interaction");
|
|
305
|
+
}
|
|
306
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
307
|
+
if (existingActiveBridge) {
|
|
308
|
+
existingActiveBridge.diagnostics = {
|
|
309
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
310
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
311
|
+
lastMcpUpdate: `${info.updateCase}:${info.toolCallId}`,
|
|
312
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
313
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}, (info) => {
|
|
317
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
318
|
+
if (existingActiveBridge) {
|
|
319
|
+
existingActiveBridge.diagnostics = {
|
|
320
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
321
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
322
|
+
lastMcpUpdate: info.updateCase === "stepCompleted"
|
|
323
|
+
? `${info.updateCase}:${info.stepId}:${info.stepDurationMs ?? "unknown"}`
|
|
324
|
+
: `${info.updateCase}:${info.stepId}`,
|
|
325
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
326
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
logPluginInfo("Tracking Cursor step boundary in streaming bridge", {
|
|
330
|
+
modelId,
|
|
331
|
+
bridgeKey,
|
|
332
|
+
convKey,
|
|
333
|
+
updateCase: info.updateCase,
|
|
334
|
+
stepId: info.stepId,
|
|
335
|
+
stepDurationMs: info.stepDurationMs,
|
|
336
|
+
toolCallsFlushed,
|
|
337
|
+
mcpExecReceived,
|
|
338
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
|
|
339
|
+
streamedToolCallIds: sortedIds(streamedToolCalls.keys()),
|
|
340
|
+
hasStoredActiveBridge: Boolean(existingActiveBridge),
|
|
341
|
+
storedActiveBridgePendingExecToolCallIds: existingActiveBridge
|
|
342
|
+
? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
|
|
343
|
+
: [],
|
|
344
|
+
storedActiveBridgeDiagnostics: existingActiveBridge?.diagnostics,
|
|
137
345
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
346
|
+
}, (checkpointBytes) => {
|
|
347
|
+
logPluginInfo("Received Cursor conversation checkpoint", {
|
|
348
|
+
modelId,
|
|
349
|
+
bridgeKey,
|
|
350
|
+
convKey,
|
|
351
|
+
toolCallsFlushed,
|
|
352
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
|
|
353
|
+
});
|
|
354
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
355
|
+
bridgeCloseController.noteCheckpoint();
|
|
356
|
+
if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
|
|
357
|
+
publishPendingToolCalls("checkpoint");
|
|
358
|
+
}
|
|
359
|
+
}, () => {
|
|
360
|
+
logPluginInfo("Received Cursor turn-ended signal", {
|
|
361
|
+
modelId,
|
|
362
|
+
bridgeKey,
|
|
363
|
+
convKey,
|
|
364
|
+
toolCallsFlushed,
|
|
365
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
|
|
366
|
+
});
|
|
367
|
+
bridgeCloseController.noteTurnEnded();
|
|
368
|
+
if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
|
|
369
|
+
publishPendingToolCalls("turnEnded");
|
|
370
|
+
}
|
|
371
|
+
}, (info) => {
|
|
372
|
+
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
373
|
+
logPluginError("Closing Cursor bridge after unsupported message", {
|
|
374
|
+
modelId,
|
|
375
|
+
bridgeKey,
|
|
376
|
+
convKey,
|
|
377
|
+
category: info.category,
|
|
378
|
+
caseName: info.caseName,
|
|
379
|
+
detail: info.detail,
|
|
380
|
+
});
|
|
381
|
+
scheduleBridgeEnd(bridge);
|
|
382
|
+
}, (info) => {
|
|
142
383
|
endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
|
|
143
384
|
logPluginError("Closing Cursor bridge after unsupported exec", {
|
|
144
385
|
modelId,
|
|
@@ -155,6 +396,15 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
155
396
|
// Skip unparseable messages.
|
|
156
397
|
}
|
|
157
398
|
}, (endStreamBytes) => {
|
|
399
|
+
logPluginInfo("Received Cursor end-of-stream signal", {
|
|
400
|
+
modelId,
|
|
401
|
+
bridgeKey,
|
|
402
|
+
convKey,
|
|
403
|
+
byteLength: endStreamBytes.length,
|
|
404
|
+
});
|
|
405
|
+
if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
|
|
406
|
+
publishPendingToolCalls("endStream");
|
|
407
|
+
}
|
|
158
408
|
endStreamError = parseConnectEndStream(endStreamBytes);
|
|
159
409
|
if (endStreamError) {
|
|
160
410
|
logPluginError("Cursor stream returned Connect end-stream error", {
|
|
@@ -180,17 +430,32 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
180
430
|
stopKeepalive();
|
|
181
431
|
}
|
|
182
432
|
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
433
|
+
logPluginInfo("Opened Cursor streaming bridge", {
|
|
434
|
+
modelId,
|
|
435
|
+
bridgeKey,
|
|
436
|
+
convKey,
|
|
437
|
+
mcpToolCount: mcpTools.length,
|
|
438
|
+
hasCloudRule: Boolean(cloudRule),
|
|
439
|
+
});
|
|
183
440
|
bridge.onData(processChunk);
|
|
184
441
|
bridge.onClose((code) => {
|
|
442
|
+
logPluginInfo("Cursor streaming bridge closed", {
|
|
443
|
+
modelId,
|
|
444
|
+
bridgeKey,
|
|
445
|
+
convKey,
|
|
446
|
+
code,
|
|
447
|
+
mcpExecReceived,
|
|
448
|
+
hadEndStreamError: Boolean(endStreamError),
|
|
449
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
450
|
+
storedActiveBridgeDiagnostics: activeBridges.get(bridgeKey)?.diagnostics,
|
|
451
|
+
});
|
|
452
|
+
bridgeCloseController.dispose();
|
|
185
453
|
clearInterval(heartbeatTimer);
|
|
186
454
|
stopKeepalive();
|
|
187
455
|
syncStoredBlobStore(convKey, blobStore);
|
|
188
456
|
if (endStreamError) {
|
|
189
457
|
activeBridges.delete(bridgeKey);
|
|
190
|
-
|
|
191
|
-
closed = true;
|
|
192
|
-
controller.error(endStreamError);
|
|
193
|
-
}
|
|
458
|
+
failStream(endStreamError.message, "cursor_bridge_closed");
|
|
194
459
|
return;
|
|
195
460
|
}
|
|
196
461
|
if (!mcpExecReceived) {
|
|
@@ -210,15 +475,12 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
210
475
|
}
|
|
211
476
|
activeBridges.delete(bridgeKey);
|
|
212
477
|
if (code !== 0 && !closed) {
|
|
213
|
-
|
|
214
|
-
sendSSE(makeChunk({}, "stop"));
|
|
215
|
-
sendSSE(makeUsageChunk());
|
|
216
|
-
sendDone();
|
|
217
|
-
closeController();
|
|
478
|
+
failStream("Cursor bridge connection lost", "cursor_bridge_closed");
|
|
218
479
|
}
|
|
219
480
|
});
|
|
220
481
|
},
|
|
221
482
|
cancel(reason) {
|
|
483
|
+
bridgeCloseController.dispose();
|
|
222
484
|
stopKeepalive();
|
|
223
485
|
clearInterval(heartbeatTimer);
|
|
224
486
|
syncStoredBlobStore(convKey, blobStore);
|
|
@@ -238,11 +500,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
238
500
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
239
501
|
}
|
|
240
502
|
export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
503
|
+
logPluginInfo("Starting Cursor streaming response", {
|
|
504
|
+
modelId,
|
|
505
|
+
bridgeKey,
|
|
506
|
+
convKey,
|
|
507
|
+
mcpToolCount: payload.mcpTools.length,
|
|
508
|
+
});
|
|
241
509
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
242
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
510
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
243
511
|
}
|
|
244
|
-
|
|
245
|
-
const
|
|
512
|
+
async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
|
|
513
|
+
const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
514
|
+
const deadline = Date.now() + timeoutMs;
|
|
515
|
+
while (Date.now() < deadline) {
|
|
516
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
517
|
+
if (unresolved.length === 0) {
|
|
518
|
+
return unresolved;
|
|
519
|
+
}
|
|
520
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
521
|
+
}
|
|
522
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
523
|
+
if (unresolved.length > 0) {
|
|
524
|
+
logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
|
|
525
|
+
bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
|
|
526
|
+
modelId: active.modelId,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return unresolved;
|
|
530
|
+
}
|
|
531
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
532
|
+
const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
246
533
|
const resumeMetadata = {
|
|
247
534
|
...metadata,
|
|
248
535
|
assistantSeedText: [
|
|
@@ -252,7 +539,73 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
252
539
|
.filter(Boolean)
|
|
253
540
|
.join("\n\n"),
|
|
254
541
|
};
|
|
255
|
-
|
|
542
|
+
const pendingToolCallIds = new Set(pendingExecs.map((exec) => exec.toolCallId));
|
|
543
|
+
const toolResultIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
544
|
+
const missingToolResultIds = pendingExecs
|
|
545
|
+
.map((exec) => exec.toolCallId)
|
|
546
|
+
.filter((toolCallId) => !toolResultIds.has(toolCallId));
|
|
547
|
+
const unexpectedToolResultIds = toolResults
|
|
548
|
+
.map((result) => result.toolCallId)
|
|
549
|
+
.filter((toolCallId) => !pendingToolCallIds.has(toolCallId));
|
|
550
|
+
const matchedPendingExecs = pendingExecs.filter((exec) => toolResultIds.has(exec.toolCallId));
|
|
551
|
+
logPluginInfo("Preparing Cursor tool-result resume", {
|
|
552
|
+
bridgeKey,
|
|
553
|
+
convKey,
|
|
554
|
+
modelId,
|
|
555
|
+
toolResults,
|
|
556
|
+
pendingExecs,
|
|
557
|
+
bridgeAlive: bridge.alive,
|
|
558
|
+
diagnostics: active.diagnostics,
|
|
559
|
+
});
|
|
560
|
+
if (active.diagnostics) {
|
|
561
|
+
active.diagnostics.lastResumeAttemptAtMs = Date.now();
|
|
562
|
+
}
|
|
563
|
+
const unresolved = await waitForResolvablePendingExecs(active, toolResults);
|
|
564
|
+
logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
|
|
565
|
+
bridgeKey,
|
|
566
|
+
convKey,
|
|
567
|
+
modelId,
|
|
568
|
+
toolResults,
|
|
569
|
+
pendingExecs,
|
|
570
|
+
unresolvedPendingExecs: unresolved,
|
|
571
|
+
diagnostics: active.diagnostics,
|
|
572
|
+
});
|
|
573
|
+
if (unresolved.length > 0) {
|
|
574
|
+
clearInterval(heartbeatTimer);
|
|
575
|
+
bridge.end();
|
|
576
|
+
return new Response(JSON.stringify({
|
|
577
|
+
error: {
|
|
578
|
+
message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
|
|
579
|
+
type: "invalid_request_error",
|
|
580
|
+
code: "cursor_missing_exec_metadata",
|
|
581
|
+
},
|
|
582
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
583
|
+
}
|
|
584
|
+
if (missingToolResultIds.length > 0 || unexpectedToolResultIds.length > 0) {
|
|
585
|
+
logPluginError("Aborting Cursor tool-result resume because tool-call ids did not match", {
|
|
586
|
+
bridgeKey,
|
|
587
|
+
convKey,
|
|
588
|
+
modelId,
|
|
589
|
+
pendingToolCallIds: [...pendingToolCallIds],
|
|
590
|
+
toolResultIds: [...toolResultIds],
|
|
591
|
+
missingToolResultIds,
|
|
592
|
+
unexpectedToolResultIds,
|
|
593
|
+
});
|
|
594
|
+
clearInterval(heartbeatTimer);
|
|
595
|
+
bridge.end();
|
|
596
|
+
return new Response(JSON.stringify({
|
|
597
|
+
error: {
|
|
598
|
+
message: "Tool-result ids did not match the active Cursor MCP tool calls. Aborting instead of resuming a potentially stuck session.",
|
|
599
|
+
type: "invalid_request_error",
|
|
600
|
+
code: "cursor_tool_result_mismatch",
|
|
601
|
+
details: {
|
|
602
|
+
missingToolResultIds,
|
|
603
|
+
unexpectedToolResultIds,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
607
|
+
}
|
|
608
|
+
for (const exec of matchedPendingExecs) {
|
|
256
609
|
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
257
610
|
const mcpResult = result
|
|
258
611
|
? create(McpResultSchema, {
|
|
@@ -292,7 +645,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
292
645
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
293
646
|
message: { case: "execClientMessage", value: execClientMessage },
|
|
294
647
|
});
|
|
648
|
+
logPluginInfo("Sending Cursor tool-result resume message", {
|
|
649
|
+
bridgeKey,
|
|
650
|
+
convKey,
|
|
651
|
+
modelId,
|
|
652
|
+
toolCallId: exec.toolCallId,
|
|
653
|
+
toolName: exec.toolName,
|
|
654
|
+
source: exec.source,
|
|
655
|
+
execId: exec.execId,
|
|
656
|
+
execMsgId: exec.execMsgId,
|
|
657
|
+
cursorCallId: exec.cursorCallId,
|
|
658
|
+
modelCallId: exec.modelCallId,
|
|
659
|
+
matchedToolResult: result,
|
|
660
|
+
});
|
|
295
661
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
296
662
|
}
|
|
297
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
663
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
298
664
|
}
|
|
@@ -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,24 @@ 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
|
+
diagnostics: activeBridge.diagnostics,
|
|
71
|
+
});
|
|
42
72
|
activeBridges.delete(bridgeKey);
|
|
43
73
|
if (activeBridge.bridge.alive) {
|
|
44
74
|
if (activeBridge.modelId !== modelId) {
|
|
@@ -82,16 +112,27 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
82
112
|
// Build the request. When tool results are present but the bridge died,
|
|
83
113
|
// we must still include the last user text so Cursor has context.
|
|
84
114
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
115
|
+
const hasPendingAssistantSummary = pendingAssistantSummary.trim().length > 0;
|
|
85
116
|
const needsInitialHandoff = !stored.checkpoint &&
|
|
86
|
-
(turns.length > 0 ||
|
|
117
|
+
(turns.length > 0 || hasPendingAssistantSummary || toolResults.length > 0);
|
|
87
118
|
const replayTurns = needsInitialHandoff ? [] : turns;
|
|
88
119
|
let effectiveUserText = needsInitialHandoff
|
|
89
120
|
? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
|
|
90
|
-
: toolResults.length > 0
|
|
121
|
+
: toolResults.length > 0 || hasPendingAssistantSummary
|
|
91
122
|
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
92
123
|
: userText;
|
|
93
124
|
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
94
125
|
payload.mcpTools = mcpTools;
|
|
126
|
+
logPluginInfo("Built Cursor run request payload", {
|
|
127
|
+
modelId,
|
|
128
|
+
bridgeKey,
|
|
129
|
+
convKey,
|
|
130
|
+
mcpToolCount: mcpTools.length,
|
|
131
|
+
conversationId: stored.conversationId,
|
|
132
|
+
hasCheckpoint: Boolean(stored.checkpoint),
|
|
133
|
+
replayTurnCount: replayTurns.length,
|
|
134
|
+
effectiveUserText,
|
|
135
|
+
});
|
|
95
136
|
if (body.stream === false) {
|
|
96
137
|
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
|
|
97
138
|
systemPrompt,
|