@playwo/opencode-cursor-oauth 0.0.0-dev.12f4642b8f6b → 0.0.0-dev.14c6316643ec
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cursor/bidi-session.d.ts +1 -2
- package/dist/cursor/bidi-session.js +153 -138
- package/dist/cursor/index.d.ts +1 -1
- package/dist/cursor/index.js +1 -1
- package/dist/cursor/unary-rpc.d.ts +0 -1
- package/dist/cursor/unary-rpc.js +2 -59
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +3 -0
- package/dist/openai/messages.js +5 -0
- package/dist/plugin/cursor-auth-plugin.js +0 -1
- package/dist/proxy/bridge-close-controller.d.ts +6 -0
- package/dist/proxy/bridge-close-controller.js +37 -0
- package/dist/proxy/bridge-non-streaming.js +18 -2
- package/dist/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +129 -18
- package/dist/proxy/chat-completion.js +43 -3
- package/dist/proxy/cursor-request.d.ts +1 -0
- package/dist/proxy/cursor-request.js +23 -8
- package/dist/proxy/server.js +23 -5
- package/dist/proxy/stream-dispatch.d.ts +6 -1
- package/dist/proxy/stream-dispatch.js +241 -11
- package/dist/proxy/types.d.ts +6 -1
- package/package.json +1 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type CursorBaseRequestOptions } from "./headers";
|
|
2
|
-
export declare function encodeBidiAppendRequest(dataHex: string, requestId: string, appendSeqno: number): Uint8Array;
|
|
3
2
|
export interface CursorSession {
|
|
4
3
|
write: (data: Uint8Array) => void;
|
|
5
4
|
end: () => void;
|
|
@@ -8,6 +7,6 @@ export interface CursorSession {
|
|
|
8
7
|
readonly alive: boolean;
|
|
9
8
|
}
|
|
10
9
|
export interface CreateCursorSessionOptions extends CursorBaseRequestOptions {
|
|
11
|
-
|
|
10
|
+
initialRequestBytes: Uint8Array;
|
|
12
11
|
}
|
|
13
12
|
export declare function createCursorSession(options: CreateCursorSessionOptions): Promise<CursorSession>;
|
|
@@ -1,149 +1,164 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
export function encodeBidiAppendRequest(dataHex, requestId, appendSeqno) {
|
|
8
|
-
const requestIdBytes = toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId }));
|
|
9
|
-
return concatBytes([
|
|
10
|
-
encodeProtoStringField(1, dataHex),
|
|
11
|
-
encodeProtoMessageField(2, requestIdBytes),
|
|
12
|
-
encodeProtoVarintField(3, appendSeqno),
|
|
13
|
-
]);
|
|
14
|
-
}
|
|
1
|
+
import { connect as connectHttp2, } from "node:http2";
|
|
2
|
+
import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
|
|
3
|
+
import { frameConnectMessage } from "./connect-framing";
|
|
4
|
+
import { buildCursorHeaderValues, } from "./headers";
|
|
5
|
+
import { errorDetails, logPluginError } from "../logger";
|
|
6
|
+
const CURSOR_BIDI_RUN_PATH = "/agent.v1.AgentService/Run";
|
|
15
7
|
export async function createCursorSession(options) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
headers: buildCursorHeaders(options, "application/connect+proto", {
|
|
19
|
-
accept: "text/event-stream",
|
|
20
|
-
"connect-protocol-version": "1",
|
|
21
|
-
}),
|
|
22
|
-
body: toFetchBody(frameConnectMessage(toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId: options.requestId })))),
|
|
23
|
-
});
|
|
24
|
-
if (!response.ok || !response.body) {
|
|
25
|
-
const errorBody = await response.text().catch(() => "");
|
|
26
|
-
logPluginError("Cursor RunSSE request failed", {
|
|
27
|
-
requestId: options.requestId,
|
|
28
|
-
status: response.status,
|
|
29
|
-
responseBody: errorBody,
|
|
30
|
-
});
|
|
31
|
-
throw new Error(`RunSSE failed: ${response.status}${errorBody ? ` ${errorBody}` : ""}`);
|
|
8
|
+
if (options.initialRequestBytes.length === 0) {
|
|
9
|
+
throw new Error("Cursor sessions require an initial request message");
|
|
32
10
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const appendResponse = await fetch(new URL("/aiserver.v1.BidiService/BidiAppend", options.url ?? CURSOR_API_URL), {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: buildCursorHeaders(options, "application/proto"),
|
|
56
|
-
body: toFetchBody(requestBody),
|
|
57
|
-
signal: abortController.signal,
|
|
58
|
-
});
|
|
59
|
-
if (!appendResponse.ok) {
|
|
60
|
-
const errorBody = await appendResponse.text().catch(() => "");
|
|
61
|
-
logPluginError("Cursor BidiAppend request failed", {
|
|
62
|
-
requestId: options.requestId,
|
|
63
|
-
appendSeqno: appendSeqno - 1,
|
|
64
|
-
status: appendResponse.status,
|
|
65
|
-
responseBody: errorBody,
|
|
66
|
-
});
|
|
67
|
-
throw new Error(`BidiAppend failed: ${appendResponse.status}${errorBody ? ` ${errorBody}` : ""}`);
|
|
68
|
-
}
|
|
69
|
-
await appendResponse.arrayBuffer().catch(() => undefined);
|
|
70
|
-
};
|
|
71
|
-
(async () => {
|
|
72
|
-
try {
|
|
73
|
-
while (true) {
|
|
74
|
-
const { done, value } = await reader.read();
|
|
75
|
-
if (done) {
|
|
76
|
-
finish(0);
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
if (value && value.length > 0) {
|
|
80
|
-
const chunk = Buffer.from(value);
|
|
81
|
-
if (cbs.data) {
|
|
82
|
-
cbs.data(chunk);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
pendingChunks.push(chunk);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
11
|
+
const target = new URL(CURSOR_BIDI_RUN_PATH, options.url ?? CURSOR_API_URL);
|
|
12
|
+
const authority = `${target.protocol}//${target.host}`;
|
|
13
|
+
const requestId = crypto.randomUUID();
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const cbs = {
|
|
16
|
+
data: null,
|
|
17
|
+
close: null,
|
|
18
|
+
};
|
|
19
|
+
let session;
|
|
20
|
+
let stream;
|
|
21
|
+
let alive = true;
|
|
22
|
+
let closeCode = 0;
|
|
23
|
+
let opened = false;
|
|
24
|
+
let settled = false;
|
|
25
|
+
let statusCode = 0;
|
|
26
|
+
const pendingChunks = [];
|
|
27
|
+
const errorChunks = [];
|
|
28
|
+
const closeTransport = () => {
|
|
29
|
+
try {
|
|
30
|
+
stream?.close();
|
|
88
31
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
32
|
+
catch { }
|
|
33
|
+
try {
|
|
34
|
+
session?.close();
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
};
|
|
38
|
+
const finish = (code) => {
|
|
39
|
+
if (!alive)
|
|
40
|
+
return;
|
|
41
|
+
alive = false;
|
|
42
|
+
closeCode = code;
|
|
43
|
+
cbs.close?.(code);
|
|
44
|
+
closeTransport();
|
|
45
|
+
};
|
|
46
|
+
const rejectOpen = (error) => {
|
|
47
|
+
if (settled)
|
|
48
|
+
return;
|
|
49
|
+
settled = true;
|
|
50
|
+
alive = false;
|
|
51
|
+
closeTransport();
|
|
52
|
+
reject(error);
|
|
53
|
+
};
|
|
54
|
+
const resolveOpen = (sessionHandle) => {
|
|
55
|
+
if (settled)
|
|
56
|
+
return;
|
|
57
|
+
settled = true;
|
|
58
|
+
opened = true;
|
|
59
|
+
resolve(sessionHandle);
|
|
60
|
+
};
|
|
61
|
+
const handleTransportError = (message, error) => {
|
|
62
|
+
logPluginError(message, {
|
|
63
|
+
requestId,
|
|
64
|
+
url: target.toString(),
|
|
93
65
|
...errorDetails(error),
|
|
94
66
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})();
|
|
98
|
-
return {
|
|
99
|
-
get alive() {
|
|
100
|
-
return alive;
|
|
101
|
-
},
|
|
102
|
-
write(data) {
|
|
103
|
-
if (!alive)
|
|
67
|
+
if (!opened) {
|
|
68
|
+
rejectOpen(new Error(error instanceof Error ? error.message : String(error ?? message)));
|
|
104
69
|
return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
70
|
+
}
|
|
71
|
+
finish(1);
|
|
72
|
+
};
|
|
73
|
+
const sessionHandle = {
|
|
74
|
+
get alive() {
|
|
75
|
+
return alive;
|
|
76
|
+
},
|
|
77
|
+
write(data) {
|
|
78
|
+
if (!alive || !stream)
|
|
79
|
+
return;
|
|
112
80
|
try {
|
|
113
|
-
|
|
81
|
+
stream.write(frameConnectMessage(data));
|
|
114
82
|
}
|
|
115
|
-
catch {
|
|
116
|
-
|
|
117
|
-
|
|
83
|
+
catch (error) {
|
|
84
|
+
handleTransportError("Cursor HTTP/2 write failed", error);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
end() {
|
|
88
|
+
finish(0);
|
|
89
|
+
},
|
|
90
|
+
onData(cb) {
|
|
91
|
+
cbs.data = cb;
|
|
92
|
+
while (pendingChunks.length > 0) {
|
|
93
|
+
cb(pendingChunks.shift());
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onClose(cb) {
|
|
97
|
+
if (!alive) {
|
|
98
|
+
queueMicrotask(() => cb(closeCode));
|
|
118
99
|
}
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
else {
|
|
101
|
+
cbs.close = cb;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
session = connectHttp2(authority);
|
|
107
|
+
session.once("error", (error) => {
|
|
108
|
+
handleTransportError("Cursor HTTP/2 session failed", error);
|
|
121
109
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
110
|
+
const headers = {
|
|
111
|
+
":method": "POST",
|
|
112
|
+
":path": `${target.pathname}${target.search}`,
|
|
113
|
+
...buildCursorHeaderValues(options, "application/connect+proto", {
|
|
114
|
+
accept: "application/connect+proto",
|
|
115
|
+
"connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
stream = session.request(headers);
|
|
119
|
+
stream.once("response", (responseHeaders) => {
|
|
120
|
+
const statusHeader = responseHeaders[":status"];
|
|
121
|
+
statusCode =
|
|
122
|
+
typeof statusHeader === "number"
|
|
123
|
+
? statusHeader
|
|
124
|
+
: Number(statusHeader ?? 0);
|
|
125
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
126
|
+
resolveOpen(sessionHandle);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
stream.on("data", (chunk) => {
|
|
130
|
+
const buffer = Buffer.from(chunk);
|
|
131
|
+
if (!opened && statusCode >= 400) {
|
|
132
|
+
errorChunks.push(buffer);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (cbs.data) {
|
|
136
|
+
cbs.data(buffer);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
pendingChunks.push(buffer);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
stream.once("end", () => {
|
|
143
|
+
if (!opened) {
|
|
144
|
+
const errorBody = Buffer.concat(errorChunks).toString("utf8").trim();
|
|
145
|
+
logPluginError("Cursor HTTP/2 Run request failed", {
|
|
146
|
+
requestId,
|
|
147
|
+
status: statusCode,
|
|
148
|
+
responseBody: errorBody,
|
|
149
|
+
});
|
|
150
|
+
rejectOpen(new Error(`Run failed: ${statusCode || 1}${errorBody ? ` ${errorBody}` : ""}`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
finish(statusCode >= 200 && statusCode < 300 ? 0 : statusCode || 1);
|
|
154
|
+
});
|
|
155
|
+
stream.once("error", (error) => {
|
|
156
|
+
handleTransportError("Cursor HTTP/2 stream failed", error);
|
|
157
|
+
});
|
|
158
|
+
stream.write(frameConnectMessage(options.initialRequestBytes));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
handleTransportError("Cursor HTTP/2 transport setup failed", error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
149
164
|
}
|
package/dist/cursor/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
|
|
2
2
|
export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
|
|
3
3
|
export { buildCursorHeaders, buildCursorHeaderValues, type CursorBaseRequestOptions, } from "./headers";
|
|
4
|
-
export { createCursorSession,
|
|
4
|
+
export { createCursorSession, type CreateCursorSessionOptions, type CursorSession, } from "./bidi-session";
|
|
5
5
|
export { callCursorUnaryRpc, type CursorUnaryRpcOptions } from "./unary-rpc";
|
package/dist/cursor/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
|
|
2
2
|
export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
|
|
3
3
|
export { buildCursorHeaders, buildCursorHeaderValues, } from "./headers";
|
|
4
|
-
export { createCursorSession,
|
|
4
|
+
export { createCursorSession, } from "./bidi-session";
|
|
5
5
|
export { callCursorUnaryRpc } from "./unary-rpc";
|
package/dist/cursor/unary-rpc.js
CHANGED
|
@@ -1,67 +1,10 @@
|
|
|
1
1
|
import { connect as connectHttp2, } from "node:http2";
|
|
2
2
|
import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
|
|
3
|
-
import {
|
|
4
|
-
import { buildCursorHeaders, buildCursorHeaderValues } from "./headers";
|
|
3
|
+
import { buildCursorHeaderValues } from "./headers";
|
|
5
4
|
import { errorDetails, logPluginError } from "../logger";
|
|
6
5
|
export async function callCursorUnaryRpc(options) {
|
|
7
6
|
const target = new URL(options.rpcPath, options.url ?? CURSOR_API_URL);
|
|
8
|
-
|
|
9
|
-
if (transport === "http2" ||
|
|
10
|
-
(transport === "auto" && target.protocol === "https:")) {
|
|
11
|
-
const http2Result = await callCursorUnaryRpcOverHttp2(options, target);
|
|
12
|
-
if (transport === "http2" ||
|
|
13
|
-
http2Result.timedOut ||
|
|
14
|
-
http2Result.exitCode !== 1) {
|
|
15
|
-
return http2Result;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return callCursorUnaryRpcOverFetch(options, target);
|
|
19
|
-
}
|
|
20
|
-
async function callCursorUnaryRpcOverFetch(options, target) {
|
|
21
|
-
let timedOut = false;
|
|
22
|
-
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
23
|
-
const controller = new AbortController();
|
|
24
|
-
const timeout = timeoutMs > 0
|
|
25
|
-
? setTimeout(() => {
|
|
26
|
-
timedOut = true;
|
|
27
|
-
controller.abort();
|
|
28
|
-
}, timeoutMs)
|
|
29
|
-
: undefined;
|
|
30
|
-
try {
|
|
31
|
-
const response = await fetch(target, {
|
|
32
|
-
method: "POST",
|
|
33
|
-
headers: buildCursorHeaders(options, "application/proto", {
|
|
34
|
-
accept: "application/proto, application/json",
|
|
35
|
-
"connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
|
|
36
|
-
"connect-timeout-ms": String(timeoutMs),
|
|
37
|
-
}),
|
|
38
|
-
body: toFetchBody(options.requestBody),
|
|
39
|
-
signal: controller.signal,
|
|
40
|
-
});
|
|
41
|
-
const body = new Uint8Array(await response.arrayBuffer());
|
|
42
|
-
return {
|
|
43
|
-
body,
|
|
44
|
-
exitCode: response.ok ? 0 : response.status,
|
|
45
|
-
timedOut,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
logPluginError("Cursor unary fetch transport failed", {
|
|
50
|
-
rpcPath: options.rpcPath,
|
|
51
|
-
url: target.toString(),
|
|
52
|
-
timeoutMs,
|
|
53
|
-
timedOut,
|
|
54
|
-
});
|
|
55
|
-
return {
|
|
56
|
-
body: new Uint8Array(),
|
|
57
|
-
exitCode: timedOut ? 124 : 1,
|
|
58
|
-
timedOut,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
if (timeout)
|
|
63
|
-
clearTimeout(timeout);
|
|
64
|
-
}
|
|
7
|
+
return callCursorUnaryRpcOverHttp2(options, target);
|
|
65
8
|
}
|
|
66
9
|
async function callCursorUnaryRpcOverHttp2(options, target) {
|
|
67
10
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
package/dist/logger.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import type { PluginInput } from "@opencode-ai/plugin";
|
|
|
2
2
|
export declare function configurePluginLogger(input: PluginInput): void;
|
|
3
3
|
export declare function errorDetails(error: unknown): Record<string, unknown>;
|
|
4
4
|
export declare function logPluginWarn(message: string, extra?: Record<string, unknown>): void;
|
|
5
|
+
export declare function logPluginInfo(message: string, extra?: Record<string, unknown>): void;
|
|
5
6
|
export declare function logPluginError(message: string, extra?: Record<string, unknown>): void;
|
|
6
7
|
export declare function flushPluginLogs(): Promise<void>;
|
package/dist/logger.js
CHANGED
|
@@ -27,6 +27,9 @@ export function errorDetails(error) {
|
|
|
27
27
|
export function logPluginWarn(message, extra = {}) {
|
|
28
28
|
logPlugin("warn", message, extra);
|
|
29
29
|
}
|
|
30
|
+
export function logPluginInfo(message, extra = {}) {
|
|
31
|
+
logPlugin("info", message, extra);
|
|
32
|
+
}
|
|
30
33
|
export function logPluginError(message, extra = {}) {
|
|
31
34
|
logPlugin("error", message, extra);
|
|
32
35
|
}
|
package/dist/openai/messages.js
CHANGED
|
@@ -79,6 +79,11 @@ export function parseMessages(messages) {
|
|
|
79
79
|
completedTurnStates = parsedTurns.slice(0, -1);
|
|
80
80
|
userText = lastTurn.userText;
|
|
81
81
|
}
|
|
82
|
+
else if (lastTurn.userText && hasAssistantSummary) {
|
|
83
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
84
|
+
userText = lastTurn.userText;
|
|
85
|
+
pendingAssistantSummary = summarizeTurnSegments(lastTurn.segments);
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
const turns = completedTurnStates
|
|
84
89
|
.map((turn) => ({
|
|
@@ -110,7 +110,6 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
110
110
|
async "chat.headers"(incoming, output) {
|
|
111
111
|
if (incoming.model.providerID !== CURSOR_PROVIDER_ID)
|
|
112
112
|
return;
|
|
113
|
-
output.headers["x-opencode-session-id"] = incoming.sessionID;
|
|
114
113
|
output.headers["x-session-id"] = incoming.sessionID;
|
|
115
114
|
if (incoming.agent) {
|
|
116
115
|
output.headers["x-opencode-agent"] = incoming.agent;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { scheduleBridgeEnd } from "./stream-dispatch";
|
|
2
|
+
const TURN_END_GRACE_MS = 750;
|
|
3
|
+
export function createBridgeCloseController(bridge) {
|
|
4
|
+
let turnEnded = false;
|
|
5
|
+
let checkpointSeen = false;
|
|
6
|
+
let closeTimer;
|
|
7
|
+
const clearCloseTimer = () => {
|
|
8
|
+
if (!closeTimer)
|
|
9
|
+
return;
|
|
10
|
+
clearTimeout(closeTimer);
|
|
11
|
+
closeTimer = undefined;
|
|
12
|
+
};
|
|
13
|
+
const closeBridge = () => {
|
|
14
|
+
clearCloseTimer();
|
|
15
|
+
scheduleBridgeEnd(bridge);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
noteTurnEnded() {
|
|
19
|
+
turnEnded = true;
|
|
20
|
+
if (checkpointSeen) {
|
|
21
|
+
closeBridge();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
clearCloseTimer();
|
|
25
|
+
closeTimer = setTimeout(closeBridge, TURN_END_GRACE_MS);
|
|
26
|
+
},
|
|
27
|
+
noteCheckpoint() {
|
|
28
|
+
checkpointSeen = true;
|
|
29
|
+
if (turnEnded) {
|
|
30
|
+
closeBridge();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
dispose() {
|
|
34
|
+
clearCloseTimer();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -5,6 +5,7 @@ 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";
|
|
8
9
|
export async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
|
|
9
10
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
10
11
|
const created = Math.floor(Date.now() / 1000);
|
|
@@ -33,6 +34,7 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
33
34
|
let endStreamError = null;
|
|
34
35
|
const pendingToolCalls = [];
|
|
35
36
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
37
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
36
38
|
const state = {
|
|
37
39
|
toolCallIndex: 0,
|
|
38
40
|
pendingExecs: [],
|
|
@@ -43,7 +45,7 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
43
45
|
bridge.onData(createConnectFrameParser((messageBytes) => {
|
|
44
46
|
try {
|
|
45
47
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
46
|
-
processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
48
|
+
processServerMessage(serverMessage, payload.blobStore, payload.cloudRule, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
47
49
|
if (isThinking)
|
|
48
50
|
return;
|
|
49
51
|
const { content } = tagFilter.process(text);
|
|
@@ -58,7 +60,20 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
58
60
|
},
|
|
59
61
|
});
|
|
60
62
|
scheduleBridgeEnd(bridge);
|
|
61
|
-
}, (checkpointBytes) =>
|
|
63
|
+
}, (checkpointBytes) => {
|
|
64
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
65
|
+
bridgeCloseController.noteCheckpoint();
|
|
66
|
+
}, () => bridgeCloseController.noteTurnEnded(), (info) => {
|
|
67
|
+
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
68
|
+
logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
|
|
69
|
+
modelId,
|
|
70
|
+
convKey,
|
|
71
|
+
category: info.category,
|
|
72
|
+
caseName: info.caseName,
|
|
73
|
+
detail: info.detail,
|
|
74
|
+
});
|
|
75
|
+
scheduleBridgeEnd(bridge);
|
|
76
|
+
}, (info) => {
|
|
62
77
|
endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
|
|
63
78
|
logPluginError("Closing non-streaming Cursor bridge after unsupported exec", {
|
|
64
79
|
modelId,
|
|
@@ -85,6 +100,7 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
85
100
|
scheduleBridgeEnd(bridge);
|
|
86
101
|
}));
|
|
87
102
|
bridge.onClose(() => {
|
|
103
|
+
bridgeCloseController.dispose();
|
|
88
104
|
clearInterval(heartbeatTimer);
|
|
89
105
|
syncStoredBlobStore(convKey, payload.blobStore);
|
|
90
106
|
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>;
|