@playwo/opencode-cursor-oauth 0.0.0-dev.4463bb589222 → 0.0.0-dev.4696faa690e4
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/logger.d.ts +1 -0
- package/dist/logger.js +3 -0
- 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 +31 -7
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +377 -57
- package/dist/proxy/chat-completion.js +41 -1
- package/dist/proxy/cursor-request.js +13 -15
- package/dist/proxy/stream-dispatch.d.ts +7 -1
- package/dist/proxy/stream-dispatch.js +134 -53
- package/dist/proxy/stream-state.d.ts +0 -2
- package/dist/proxy/types.d.ts +14 -1
- package/package.json +2 -3
|
@@ -1,23 +1,36 @@
|
|
|
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
|
-
|
|
12
|
+
const MCP_TOOL_BATCH_WINDOW_MS = 150;
|
|
13
|
+
function sortedIds(values) {
|
|
14
|
+
return [...values].sort();
|
|
15
|
+
}
|
|
16
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
12
17
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
13
18
|
const created = Math.floor(Date.now() / 1000);
|
|
14
19
|
let keepaliveTimer;
|
|
20
|
+
let toolCallFlushTimer;
|
|
21
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
15
22
|
const stopKeepalive = () => {
|
|
16
23
|
if (!keepaliveTimer)
|
|
17
24
|
return;
|
|
18
25
|
clearInterval(keepaliveTimer);
|
|
19
26
|
keepaliveTimer = undefined;
|
|
20
27
|
};
|
|
28
|
+
const stopToolCallFlushTimer = () => {
|
|
29
|
+
if (!toolCallFlushTimer)
|
|
30
|
+
return;
|
|
31
|
+
clearTimeout(toolCallFlushTimer);
|
|
32
|
+
toolCallFlushTimer = undefined;
|
|
33
|
+
};
|
|
21
34
|
const stream = new ReadableStream({
|
|
22
35
|
start(controller) {
|
|
23
36
|
const encoder = new TextEncoder();
|
|
@@ -27,13 +40,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
27
40
|
pendingExecs: [],
|
|
28
41
|
outputTokens: 0,
|
|
29
42
|
totalTokens: 0,
|
|
30
|
-
interactionToolArgsText: new Map(),
|
|
31
|
-
emittedToolCallIds: new Set(),
|
|
32
43
|
};
|
|
33
44
|
const tagFilter = createThinkingTagFilter();
|
|
34
45
|
let assistantText = metadata.assistantSeedText ?? "";
|
|
35
46
|
let mcpExecReceived = false;
|
|
36
47
|
let endStreamError = null;
|
|
48
|
+
let toolCallsFlushed = false;
|
|
49
|
+
const streamedToolCalls = new Map();
|
|
37
50
|
const sendSSE = (data) => {
|
|
38
51
|
if (closed)
|
|
39
52
|
return;
|
|
@@ -67,6 +80,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
67
80
|
return;
|
|
68
81
|
closed = true;
|
|
69
82
|
stopKeepalive();
|
|
83
|
+
stopToolCallFlushTimer();
|
|
70
84
|
controller.close();
|
|
71
85
|
};
|
|
72
86
|
const makeChunk = (delta, finishReason = null) => ({
|
|
@@ -87,10 +101,157 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
87
101
|
usage: { prompt_tokens, completion_tokens, total_tokens },
|
|
88
102
|
};
|
|
89
103
|
};
|
|
104
|
+
const ensureStreamedToolCall = (toolCallId, toolName) => {
|
|
105
|
+
const existing = streamedToolCalls.get(toolCallId);
|
|
106
|
+
if (existing) {
|
|
107
|
+
if (toolName && existing.toolName !== toolName) {
|
|
108
|
+
existing.toolName = toolName;
|
|
109
|
+
}
|
|
110
|
+
return existing;
|
|
111
|
+
}
|
|
112
|
+
const createdState = {
|
|
113
|
+
index: state.toolCallIndex++,
|
|
114
|
+
toolName,
|
|
115
|
+
started: false,
|
|
116
|
+
};
|
|
117
|
+
streamedToolCalls.set(toolCallId, createdState);
|
|
118
|
+
return createdState;
|
|
119
|
+
};
|
|
120
|
+
const emitPendingToolStart = (toolCallId, toolName, source) => {
|
|
121
|
+
if (!toolName)
|
|
122
|
+
return;
|
|
123
|
+
const toolCall = ensureStreamedToolCall(toolCallId, toolName);
|
|
124
|
+
if (toolCall.started)
|
|
125
|
+
return;
|
|
126
|
+
toolCall.started = true;
|
|
127
|
+
logPluginInfo("Streaming pending Cursor tool-call start", {
|
|
128
|
+
modelId,
|
|
129
|
+
bridgeKey,
|
|
130
|
+
convKey,
|
|
131
|
+
toolCallId,
|
|
132
|
+
toolName,
|
|
133
|
+
index: toolCall.index,
|
|
134
|
+
source,
|
|
135
|
+
});
|
|
136
|
+
sendSSE(makeChunk({
|
|
137
|
+
tool_calls: [
|
|
138
|
+
{
|
|
139
|
+
index: toolCall.index,
|
|
140
|
+
id: toolCallId,
|
|
141
|
+
type: "function",
|
|
142
|
+
function: {
|
|
143
|
+
name: toolCall.toolName,
|
|
144
|
+
arguments: "",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}));
|
|
149
|
+
};
|
|
150
|
+
const publishPendingToolCalls = () => {
|
|
151
|
+
if (closed || toolCallsFlushed || state.pendingExecs.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
logPluginInfo("Evaluating Cursor tool-call publish", {
|
|
154
|
+
modelId,
|
|
155
|
+
bridgeKey,
|
|
156
|
+
convKey,
|
|
157
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
158
|
+
toolCallsFlushed,
|
|
159
|
+
mcpExecReceived,
|
|
160
|
+
nowMs: Date.now(),
|
|
161
|
+
});
|
|
162
|
+
toolCallsFlushed = true;
|
|
163
|
+
stopToolCallFlushTimer();
|
|
164
|
+
const flushed = tagFilter.flush();
|
|
165
|
+
if (flushed.reasoning)
|
|
166
|
+
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
167
|
+
if (flushed.content) {
|
|
168
|
+
assistantText += flushed.content;
|
|
169
|
+
sendSSE(makeChunk({ content: flushed.content }));
|
|
170
|
+
}
|
|
171
|
+
const assistantSeedText = [
|
|
172
|
+
assistantText.trim(),
|
|
173
|
+
state.pendingExecs
|
|
174
|
+
.map((exec) => formatToolCallSummary({
|
|
175
|
+
id: exec.toolCallId,
|
|
176
|
+
type: "function",
|
|
177
|
+
function: {
|
|
178
|
+
name: exec.toolName,
|
|
179
|
+
arguments: exec.decodedArgs,
|
|
180
|
+
},
|
|
181
|
+
}))
|
|
182
|
+
.join("\n\n"),
|
|
183
|
+
]
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.join("\n\n");
|
|
186
|
+
activeBridges.set(bridgeKey, {
|
|
187
|
+
bridge,
|
|
188
|
+
heartbeatTimer,
|
|
189
|
+
blobStore,
|
|
190
|
+
cloudRule,
|
|
191
|
+
mcpTools,
|
|
192
|
+
pendingExecs: [...state.pendingExecs],
|
|
193
|
+
modelId,
|
|
194
|
+
metadata: {
|
|
195
|
+
...metadata,
|
|
196
|
+
assistantSeedText,
|
|
197
|
+
},
|
|
198
|
+
diagnostics: {
|
|
199
|
+
announcedToolCallIds: [],
|
|
200
|
+
publishedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
|
|
201
|
+
lastMcpUpdate: "publish",
|
|
202
|
+
publishedAtMs: Date.now(),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
logPluginInfo("Publishing Cursor tool-call envelope", {
|
|
206
|
+
modelId,
|
|
207
|
+
bridgeKey,
|
|
208
|
+
convKey,
|
|
209
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
210
|
+
emittedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
|
|
211
|
+
assistantSeedTextLength: assistantSeedText.length,
|
|
212
|
+
});
|
|
213
|
+
for (const exec of state.pendingExecs) {
|
|
214
|
+
emitPendingToolStart(exec.toolCallId, exec.toolName, "publish");
|
|
215
|
+
const streamedToolCall = ensureStreamedToolCall(exec.toolCallId, exec.toolName);
|
|
216
|
+
sendSSE(makeChunk({
|
|
217
|
+
tool_calls: [
|
|
218
|
+
{
|
|
219
|
+
index: streamedToolCall.index,
|
|
220
|
+
function: {
|
|
221
|
+
arguments: exec.decodedArgs,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
logPluginInfo("Stored active Cursor bridge for tool-result resume", {
|
|
228
|
+
modelId,
|
|
229
|
+
bridgeKey,
|
|
230
|
+
convKey,
|
|
231
|
+
diagnostics: activeBridges.get(bridgeKey)?.diagnostics,
|
|
232
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
233
|
+
});
|
|
234
|
+
sendSSE(makeChunk({}, "tool_calls"));
|
|
235
|
+
sendDone();
|
|
236
|
+
closeController();
|
|
237
|
+
};
|
|
238
|
+
const schedulePendingToolCallPublish = () => {
|
|
239
|
+
if (toolCallsFlushed)
|
|
240
|
+
return;
|
|
241
|
+
stopToolCallFlushTimer();
|
|
242
|
+
logPluginInfo("Scheduling Cursor tool-call publish", {
|
|
243
|
+
modelId,
|
|
244
|
+
bridgeKey,
|
|
245
|
+
convKey,
|
|
246
|
+
delayMs: MCP_TOOL_BATCH_WINDOW_MS,
|
|
247
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
248
|
+
});
|
|
249
|
+
toolCallFlushTimer = setTimeout(publishPendingToolCalls, MCP_TOOL_BATCH_WINDOW_MS);
|
|
250
|
+
};
|
|
90
251
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
91
252
|
try {
|
|
92
253
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
93
|
-
processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
254
|
+
processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
94
255
|
if (isThinking) {
|
|
95
256
|
sendSSE(makeChunk({ reasoning_content: text }));
|
|
96
257
|
return;
|
|
@@ -103,57 +264,91 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
103
264
|
sendSSE(makeChunk({ content }));
|
|
104
265
|
}
|
|
105
266
|
}, (exec) => {
|
|
106
|
-
|
|
267
|
+
if (toolCallsFlushed) {
|
|
268
|
+
logPluginWarn("Received Cursor MCP exec after tool-call envelope was already published", {
|
|
269
|
+
modelId,
|
|
270
|
+
bridgeKey,
|
|
271
|
+
convKey,
|
|
272
|
+
toolCallId: exec.toolCallId,
|
|
273
|
+
toolName: exec.toolName,
|
|
274
|
+
publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
|
|
275
|
+
[],
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
emitPendingToolStart(exec.toolCallId, exec.toolName, "exec");
|
|
279
|
+
const pendingBefore = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
|
|
280
|
+
const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
|
|
281
|
+
if (existingIndex >= 0) {
|
|
282
|
+
state.pendingExecs[existingIndex] = exec;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
state.pendingExecs.push(exec);
|
|
286
|
+
}
|
|
107
287
|
mcpExecReceived = true;
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
288
|
+
const pendingAfter = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
|
|
289
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
290
|
+
if (existingActiveBridge) {
|
|
291
|
+
existingActiveBridge.diagnostics = {
|
|
292
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
293
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
294
|
+
lastMcpUpdate: `exec:${exec.toolCallId}`,
|
|
295
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
296
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
297
|
+
};
|
|
114
298
|
}
|
|
115
|
-
|
|
116
|
-
assistantText.trim(),
|
|
117
|
-
formatToolCallSummary({
|
|
118
|
-
id: exec.toolCallId,
|
|
119
|
-
type: "function",
|
|
120
|
-
function: {
|
|
121
|
-
name: exec.toolName,
|
|
122
|
-
arguments: exec.decodedArgs,
|
|
123
|
-
},
|
|
124
|
-
}),
|
|
125
|
-
]
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
.join("\n\n");
|
|
128
|
-
sendSSE(makeChunk({
|
|
129
|
-
tool_calls: [
|
|
130
|
-
{
|
|
131
|
-
index: state.toolCallIndex++,
|
|
132
|
-
id: exec.toolCallId,
|
|
133
|
-
type: "function",
|
|
134
|
-
function: {
|
|
135
|
-
name: exec.toolName,
|
|
136
|
-
arguments: exec.decodedArgs,
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
}));
|
|
141
|
-
activeBridges.set(bridgeKey, {
|
|
142
|
-
bridge,
|
|
143
|
-
heartbeatTimer,
|
|
144
|
-
blobStore,
|
|
145
|
-
mcpTools,
|
|
146
|
-
pendingExecs: state.pendingExecs,
|
|
299
|
+
logPluginInfo("Tracking Cursor MCP exec in streaming bridge", {
|
|
147
300
|
modelId,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
301
|
+
bridgeKey,
|
|
302
|
+
convKey,
|
|
303
|
+
toolCallId: exec.toolCallId,
|
|
304
|
+
toolName: exec.toolName,
|
|
305
|
+
pendingBefore,
|
|
306
|
+
pendingAfter,
|
|
307
|
+
hasStoredActiveBridge: Boolean(existingActiveBridge),
|
|
308
|
+
storedActiveBridgePendingExecToolCallIds: existingActiveBridge
|
|
309
|
+
? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
|
|
310
|
+
: [],
|
|
311
|
+
});
|
|
312
|
+
schedulePendingToolCallPublish();
|
|
313
|
+
}, (info) => {
|
|
314
|
+
if (toolCallsFlushed) {
|
|
315
|
+
logPluginWarn("Received Cursor MCP interaction update after tool-call envelope was already published", {
|
|
316
|
+
modelId,
|
|
317
|
+
bridgeKey,
|
|
318
|
+
convKey,
|
|
319
|
+
updateCase: info.updateCase,
|
|
320
|
+
toolCallId: info.toolCallId,
|
|
321
|
+
publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
|
|
322
|
+
[],
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (info.updateCase !== "toolCallCompleted") {
|
|
326
|
+
emitPendingToolStart(info.toolCallId, info.toolName ?? "", "interaction");
|
|
327
|
+
}
|
|
328
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
329
|
+
if (existingActiveBridge) {
|
|
330
|
+
existingActiveBridge.diagnostics = {
|
|
331
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
332
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
333
|
+
lastMcpUpdate: `${info.updateCase}:${info.toolCallId}`,
|
|
334
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
335
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
logPluginInfo("Tracking Cursor MCP interaction state in streaming bridge", {
|
|
339
|
+
modelId,
|
|
340
|
+
bridgeKey,
|
|
341
|
+
convKey,
|
|
342
|
+
updateCase: info.updateCase,
|
|
343
|
+
toolCallId: info.toolCallId,
|
|
344
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
|
|
345
|
+
hasStoredActiveBridge: Boolean(existingActiveBridge),
|
|
346
|
+
storedActiveBridgeDiagnostics: existingActiveBridge?.diagnostics,
|
|
152
347
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}, (
|
|
348
|
+
}, (checkpointBytes) => {
|
|
349
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
350
|
+
bridgeCloseController.noteCheckpoint();
|
|
351
|
+
}, () => bridgeCloseController.noteTurnEnded(), (info) => {
|
|
157
352
|
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
158
353
|
logPluginError("Closing Cursor bridge after unsupported message", {
|
|
159
354
|
modelId,
|
|
@@ -206,10 +401,29 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
206
401
|
stopKeepalive();
|
|
207
402
|
}
|
|
208
403
|
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
404
|
+
logPluginInfo("Opened Cursor streaming bridge", {
|
|
405
|
+
modelId,
|
|
406
|
+
bridgeKey,
|
|
407
|
+
convKey,
|
|
408
|
+
mcpToolCount: mcpTools.length,
|
|
409
|
+
hasCloudRule: Boolean(cloudRule),
|
|
410
|
+
});
|
|
209
411
|
bridge.onData(processChunk);
|
|
210
412
|
bridge.onClose((code) => {
|
|
413
|
+
logPluginInfo("Cursor streaming bridge closed", {
|
|
414
|
+
modelId,
|
|
415
|
+
bridgeKey,
|
|
416
|
+
convKey,
|
|
417
|
+
code,
|
|
418
|
+
mcpExecReceived,
|
|
419
|
+
hadEndStreamError: Boolean(endStreamError),
|
|
420
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
421
|
+
storedActiveBridgeDiagnostics: activeBridges.get(bridgeKey)?.diagnostics,
|
|
422
|
+
});
|
|
423
|
+
bridgeCloseController.dispose();
|
|
211
424
|
clearInterval(heartbeatTimer);
|
|
212
425
|
stopKeepalive();
|
|
426
|
+
stopToolCallFlushTimer();
|
|
213
427
|
syncStoredBlobStore(convKey, blobStore);
|
|
214
428
|
if (endStreamError) {
|
|
215
429
|
activeBridges.delete(bridgeKey);
|
|
@@ -238,7 +452,9 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
238
452
|
});
|
|
239
453
|
},
|
|
240
454
|
cancel(reason) {
|
|
455
|
+
bridgeCloseController.dispose();
|
|
241
456
|
stopKeepalive();
|
|
457
|
+
stopToolCallFlushTimer();
|
|
242
458
|
clearInterval(heartbeatTimer);
|
|
243
459
|
syncStoredBlobStore(convKey, blobStore);
|
|
244
460
|
const active = activeBridges.get(bridgeKey);
|
|
@@ -257,11 +473,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
257
473
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
258
474
|
}
|
|
259
475
|
export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
476
|
+
logPluginInfo("Starting Cursor streaming response", {
|
|
477
|
+
modelId,
|
|
478
|
+
bridgeKey,
|
|
479
|
+
convKey,
|
|
480
|
+
mcpToolCount: payload.mcpTools.length,
|
|
481
|
+
});
|
|
260
482
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
261
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
483
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
262
484
|
}
|
|
263
|
-
|
|
264
|
-
const
|
|
485
|
+
async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
|
|
486
|
+
const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
487
|
+
const deadline = Date.now() + timeoutMs;
|
|
488
|
+
while (Date.now() < deadline) {
|
|
489
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
490
|
+
if (unresolved.length === 0) {
|
|
491
|
+
return unresolved;
|
|
492
|
+
}
|
|
493
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
494
|
+
}
|
|
495
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
496
|
+
if (unresolved.length > 0) {
|
|
497
|
+
logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
|
|
498
|
+
bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
|
|
499
|
+
modelId: active.modelId,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return unresolved;
|
|
503
|
+
}
|
|
504
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
505
|
+
const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
265
506
|
const resumeMetadata = {
|
|
266
507
|
...metadata,
|
|
267
508
|
assistantSeedText: [
|
|
@@ -271,7 +512,73 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
271
512
|
.filter(Boolean)
|
|
272
513
|
.join("\n\n"),
|
|
273
514
|
};
|
|
274
|
-
|
|
515
|
+
const pendingToolCallIds = new Set(pendingExecs.map((exec) => exec.toolCallId));
|
|
516
|
+
const toolResultIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
517
|
+
const missingToolResultIds = pendingExecs
|
|
518
|
+
.map((exec) => exec.toolCallId)
|
|
519
|
+
.filter((toolCallId) => !toolResultIds.has(toolCallId));
|
|
520
|
+
const unexpectedToolResultIds = toolResults
|
|
521
|
+
.map((result) => result.toolCallId)
|
|
522
|
+
.filter((toolCallId) => !pendingToolCallIds.has(toolCallId));
|
|
523
|
+
const matchedPendingExecs = pendingExecs.filter((exec) => toolResultIds.has(exec.toolCallId));
|
|
524
|
+
logPluginInfo("Preparing Cursor tool-result resume", {
|
|
525
|
+
bridgeKey,
|
|
526
|
+
convKey,
|
|
527
|
+
modelId,
|
|
528
|
+
toolResults,
|
|
529
|
+
pendingExecs,
|
|
530
|
+
bridgeAlive: bridge.alive,
|
|
531
|
+
diagnostics: active.diagnostics,
|
|
532
|
+
});
|
|
533
|
+
if (active.diagnostics) {
|
|
534
|
+
active.diagnostics.lastResumeAttemptAtMs = Date.now();
|
|
535
|
+
}
|
|
536
|
+
const unresolved = await waitForResolvablePendingExecs(active, toolResults);
|
|
537
|
+
logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
|
|
538
|
+
bridgeKey,
|
|
539
|
+
convKey,
|
|
540
|
+
modelId,
|
|
541
|
+
toolResults,
|
|
542
|
+
pendingExecs,
|
|
543
|
+
unresolvedPendingExecs: unresolved,
|
|
544
|
+
diagnostics: active.diagnostics,
|
|
545
|
+
});
|
|
546
|
+
if (unresolved.length > 0) {
|
|
547
|
+
clearInterval(heartbeatTimer);
|
|
548
|
+
bridge.end();
|
|
549
|
+
return new Response(JSON.stringify({
|
|
550
|
+
error: {
|
|
551
|
+
message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
|
|
552
|
+
type: "invalid_request_error",
|
|
553
|
+
code: "cursor_missing_exec_metadata",
|
|
554
|
+
},
|
|
555
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
556
|
+
}
|
|
557
|
+
if (missingToolResultIds.length > 0 || unexpectedToolResultIds.length > 0) {
|
|
558
|
+
logPluginError("Aborting Cursor tool-result resume because tool-call ids did not match", {
|
|
559
|
+
bridgeKey,
|
|
560
|
+
convKey,
|
|
561
|
+
modelId,
|
|
562
|
+
pendingToolCallIds: [...pendingToolCallIds],
|
|
563
|
+
toolResultIds: [...toolResultIds],
|
|
564
|
+
missingToolResultIds,
|
|
565
|
+
unexpectedToolResultIds,
|
|
566
|
+
});
|
|
567
|
+
clearInterval(heartbeatTimer);
|
|
568
|
+
bridge.end();
|
|
569
|
+
return new Response(JSON.stringify({
|
|
570
|
+
error: {
|
|
571
|
+
message: "Tool-result ids did not match the active Cursor MCP tool calls. Aborting instead of resuming a potentially stuck session.",
|
|
572
|
+
type: "invalid_request_error",
|
|
573
|
+
code: "cursor_tool_result_mismatch",
|
|
574
|
+
details: {
|
|
575
|
+
missingToolResultIds,
|
|
576
|
+
unexpectedToolResultIds,
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
580
|
+
}
|
|
581
|
+
for (const exec of matchedPendingExecs) {
|
|
275
582
|
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
276
583
|
const mcpResult = result
|
|
277
584
|
? create(McpResultSchema, {
|
|
@@ -311,7 +618,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
311
618
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
312
619
|
message: { case: "execClientMessage", value: execClientMessage },
|
|
313
620
|
});
|
|
621
|
+
logPluginInfo("Sending Cursor tool-result resume message", {
|
|
622
|
+
bridgeKey,
|
|
623
|
+
convKey,
|
|
624
|
+
modelId,
|
|
625
|
+
toolCallId: exec.toolCallId,
|
|
626
|
+
toolName: exec.toolName,
|
|
627
|
+
source: exec.source,
|
|
628
|
+
execId: exec.execId,
|
|
629
|
+
execMsgId: exec.execMsgId,
|
|
630
|
+
cursorCallId: exec.cursorCallId,
|
|
631
|
+
modelCallId: exec.modelCallId,
|
|
632
|
+
matchedToolResult: result,
|
|
633
|
+
});
|
|
314
634
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
315
635
|
}
|
|
316
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
636
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
317
637
|
}
|
|
@@ -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) {
|
|
@@ -93,6 +123,16 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
93
123
|
: userText;
|
|
94
124
|
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
95
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
|
+
});
|
|
96
136
|
if (body.stream === false) {
|
|
97
137
|
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
|
|
98
138
|
systemPrompt,
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { AgentClientMessageSchema, AgentRunRequestSchema, AgentConversationTurnStructureSchema, AssistantMessageSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationTurnStructureSchema, ConversationStepSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
|
|
3
|
+
import { appendBundledAgentsRule } from "../agent-rules";
|
|
4
4
|
export function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) {
|
|
5
5
|
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);
|
|
6
|
+
const cloudRule = buildCloudRule(systemPrompt);
|
|
11
7
|
let conversationState;
|
|
12
8
|
if (checkpoint) {
|
|
13
9
|
conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
|
|
@@ -40,7 +36,7 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
|
|
|
40
36
|
turnBytes.push(toBinary(ConversationTurnStructureSchema, turnStructure));
|
|
41
37
|
}
|
|
42
38
|
conversationState = create(ConversationStateStructureSchema, {
|
|
43
|
-
rootPromptMessagesJson: [
|
|
39
|
+
rootPromptMessagesJson: [],
|
|
44
40
|
turns: turnBytes,
|
|
45
41
|
todos: [],
|
|
46
42
|
pendingToolCalls: [],
|
|
@@ -64,14 +60,11 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
|
|
|
64
60
|
value: create(UserMessageActionSchema, { userMessage }),
|
|
65
61
|
},
|
|
66
62
|
});
|
|
67
|
-
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
|
|
63
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
|
|
68
64
|
}
|
|
69
65
|
export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
|
|
70
66
|
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);
|
|
67
|
+
const cloudRule = buildCloudRule(systemPrompt);
|
|
75
68
|
const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
|
|
76
69
|
const action = create(ConversationActionSchema, {
|
|
77
70
|
action: {
|
|
@@ -79,9 +72,9 @@ export function buildCursorResumeRequest(modelId, systemPrompt, conversationId,
|
|
|
79
72
|
value: create(ResumeActionSchema, {}),
|
|
80
73
|
},
|
|
81
74
|
});
|
|
82
|
-
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
|
|
75
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
|
|
83
76
|
}
|
|
84
|
-
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
|
|
77
|
+
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule) {
|
|
85
78
|
const modelDetails = create(ModelDetailsSchema, {
|
|
86
79
|
modelId,
|
|
87
80
|
displayModelId: modelId,
|
|
@@ -99,6 +92,11 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
|
|
|
99
92
|
return {
|
|
100
93
|
requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
|
|
101
94
|
blobStore,
|
|
95
|
+
cloudRule,
|
|
102
96
|
mcpTools: [],
|
|
103
97
|
};
|
|
104
98
|
}
|
|
99
|
+
function buildCloudRule(systemPrompt) {
|
|
100
|
+
const content = systemPrompt.trim();
|
|
101
|
+
return appendBundledAgentsRule(content || undefined);
|
|
102
|
+
}
|