@playwo/opencode-cursor-oauth 0.0.0-dev.4258a6733133 → 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/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/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-session.js +1 -3
- 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 +228 -56
- package/dist/proxy/stream-state.d.ts +0 -2
- package/dist/proxy/types.d.ts +14 -1
- package/package.json +2 -3
|
@@ -5,6 +5,8 @@ import { updateStoredConversationAfterCompletion } from "./conversation-state";
|
|
|
5
5
|
import { startBridge } from "./bridge-session";
|
|
6
6
|
import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
|
|
7
7
|
import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
|
|
8
|
+
import { createBridgeCloseController } from "./bridge-close-controller";
|
|
9
|
+
const MCP_TOOL_BATCH_WINDOW_MS = 150;
|
|
8
10
|
export async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
|
|
9
11
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
10
12
|
const created = Math.floor(Date.now() / 1000);
|
|
@@ -32,35 +34,55 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
32
34
|
let fullText = "";
|
|
33
35
|
let endStreamError = null;
|
|
34
36
|
const pendingToolCalls = [];
|
|
37
|
+
let toolCallEndTimer;
|
|
35
38
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
39
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
40
|
+
const stopToolCallEndTimer = () => {
|
|
41
|
+
if (!toolCallEndTimer)
|
|
42
|
+
return;
|
|
43
|
+
clearTimeout(toolCallEndTimer);
|
|
44
|
+
toolCallEndTimer = undefined;
|
|
45
|
+
};
|
|
46
|
+
const scheduleToolCallBridgeEnd = () => {
|
|
47
|
+
stopToolCallEndTimer();
|
|
48
|
+
toolCallEndTimer = setTimeout(() => scheduleBridgeEnd(bridge), MCP_TOOL_BATCH_WINDOW_MS);
|
|
49
|
+
};
|
|
36
50
|
const state = {
|
|
37
51
|
toolCallIndex: 0,
|
|
38
52
|
pendingExecs: [],
|
|
39
53
|
outputTokens: 0,
|
|
40
54
|
totalTokens: 0,
|
|
41
|
-
interactionToolArgsText: new Map(),
|
|
42
|
-
emittedToolCallIds: new Set(),
|
|
43
55
|
};
|
|
44
56
|
const tagFilter = createThinkingTagFilter();
|
|
45
57
|
bridge.onData(createConnectFrameParser((messageBytes) => {
|
|
46
58
|
try {
|
|
47
59
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
48
|
-
processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
60
|
+
processServerMessage(serverMessage, payload.blobStore, payload.cloudRule, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
49
61
|
if (isThinking)
|
|
50
62
|
return;
|
|
51
63
|
const { content } = tagFilter.process(text);
|
|
52
64
|
fullText += content;
|
|
53
65
|
}, (exec) => {
|
|
54
|
-
|
|
66
|
+
const toolCall = {
|
|
55
67
|
id: exec.toolCallId,
|
|
56
68
|
type: "function",
|
|
57
69
|
function: {
|
|
58
70
|
name: exec.toolName,
|
|
59
71
|
arguments: exec.decodedArgs,
|
|
60
72
|
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
};
|
|
74
|
+
const existingIndex = pendingToolCalls.findIndex((call) => call.id === exec.toolCallId);
|
|
75
|
+
if (existingIndex >= 0) {
|
|
76
|
+
pendingToolCalls[existingIndex] = toolCall;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
pendingToolCalls.push(toolCall);
|
|
80
|
+
}
|
|
81
|
+
scheduleToolCallBridgeEnd();
|
|
82
|
+
}, (_info) => { }, (checkpointBytes) => {
|
|
83
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
84
|
+
bridgeCloseController.noteCheckpoint();
|
|
85
|
+
}, () => bridgeCloseController.noteTurnEnded(), (info) => {
|
|
64
86
|
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
65
87
|
logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
|
|
66
88
|
modelId,
|
|
@@ -97,6 +119,8 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
97
119
|
scheduleBridgeEnd(bridge);
|
|
98
120
|
}));
|
|
99
121
|
bridge.onClose(() => {
|
|
122
|
+
bridgeCloseController.dispose();
|
|
123
|
+
stopToolCallEndTimer();
|
|
100
124
|
clearInterval(heartbeatTimer);
|
|
101
125
|
syncStoredBlobStore(convKey, payload.blobStore);
|
|
102
126
|
const flushed = tagFilter.flush();
|
|
@@ -2,12 +2,10 @@ import { createCursorSession } from "../cursor/bidi-session";
|
|
|
2
2
|
import { makeHeartbeatBytes } from "./stream-dispatch";
|
|
3
3
|
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
4
4
|
export async function startBridge(accessToken, requestBytes) {
|
|
5
|
-
const requestId = crypto.randomUUID();
|
|
6
5
|
const bridge = await createCursorSession({
|
|
7
6
|
accessToken,
|
|
8
|
-
|
|
7
|
+
initialRequestBytes: requestBytes,
|
|
9
8
|
});
|
|
10
|
-
bridge.write(requestBytes);
|
|
11
9
|
const heartbeatTimer = setInterval(() => bridge.write(makeHeartbeatBytes()), HEARTBEAT_INTERVAL_MS);
|
|
12
10
|
return { bridge, heartbeatTimer };
|
|
13
11
|
}
|
|
@@ -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,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
|
}
|