Skip to content

Commit 0c00282

Browse files
authored
feat: Bump support for cursor SDK (#2742)
1 parent 355d0f5 commit 0c00282

41 files changed

Lines changed: 7625 additions & 10501 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"braintrust": minor
3+
---
4+
5+
feat: Bump support for cursor SDK

‎dev-packages/seinfeld/src/cassette.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ export interface RecordedRequest {
4545

4646
/** A single recorded response, as it appeared on the wire. */
4747
export interface RecordedResponse {
48+
/** Original wire chunk sizes and inter-chunk delays for interactive streams. */
49+
streamChunks?: Array<{
50+
size: number;
51+
delayMs: number;
52+
/** Complete Connect request frames received before this response chunk. */
53+
requestFrames?: number;
54+
}>;
4855
status: number;
4956
statusText?: string;
5057
headers: Record<string, string>;

‎dev-packages/seinfeld/src/format/v1.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ const recordedRequestSchema = z.object({
2929
});
3030

3131
const recordedResponseSchema = z.object({
32+
streamChunks: z
33+
.array(
34+
z.object({
35+
size: z.number().int().positive(),
36+
delayMs: z.number().nonnegative(),
37+
requestFrames: z.number().int().nonnegative().optional(),
38+
}),
39+
)
40+
.optional(),
3241
status: z.number().int().min(100).max(599),
3342
statusText: z.string().optional(),
3443
headers: z.record(z.string()),

‎dev-packages/seinfeld/src/http.ts‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,23 @@ export async function recordResponseDraft(
107107
/** Build a Fetch API `Response` from a recorded response body. */
108108
export async function buildResponse(
109109
recorded: RecordedResponse | RecordedResponseOrDraft,
110-
ctx?: { store: CassetteStore; name: string },
110+
ctx?: {
111+
store: CassetteStore;
112+
name: string;
113+
waitForRequestFrames?: (count: number) => Promise<void>;
114+
},
111115
): Promise<Response> {
112116
// Expand \n-joined set-cookie back into multiple header entries.
113117
// Strip encoding headers: stored bytes are already decoded by the fetch
114118
// layer (undici decompresses gzip/deflate before handing the body to us).
115119
// Preserving content-encoding would cause callers to attempt a second
116120
// decode of already-plain bytes, which throws a zlib error.
117121
const headers = expandSetCookieHeader(stripEncodingHeaders(recorded.headers));
118-
const init: ResponseInit = { status: recorded.status, headers };
119-
if (recorded.statusText) init.statusText = recorded.statusText;
122+
const init: ResponseInit = {
123+
status: recorded.status,
124+
headers,
125+
...(recorded.statusText ? { statusText: recorded.statusText } : {}),
126+
};
120127
// 1xx/204/304 responses must not have a body, per Fetch spec.
121128
if (isNullBodyStatus(recorded.status)) return new Response(null, init);
122129

@@ -146,6 +153,33 @@ export async function buildResponse(
146153
? recorded.body.bytes
147154
: await decodeBody(recorded.body, ctx);
148155
if (bytes.length === 0) return new Response(null, init);
156+
if (recorded.streamChunks) {
157+
// Interactive providers wait for client messages between chunks. Frame
158+
// counts preserve that ordering even if replay's client is slower than
159+
// recording's client; recorded delays alone cannot enforce it.
160+
const chunks = recorded.streamChunks;
161+
let index = 0;
162+
let offset = 0;
163+
const stream = new ReadableStream({
164+
async pull(controller) {
165+
if (index === chunks.length) {
166+
controller.close();
167+
return;
168+
}
169+
const { size, delayMs, requestFrames } = chunks[index++]!;
170+
if (requestFrames !== undefined) {
171+
if (!ctx?.waitForRequestFrames) {
172+
throw new Error("Connect replay requires request frame tracking");
173+
}
174+
await ctx.waitForRequestFrames(requestFrames);
175+
}
176+
await new Promise((resolve) => setTimeout(resolve, delayMs));
177+
controller.enqueue(bytes.slice(offset, offset + size));
178+
offset += size;
179+
},
180+
});
181+
return new Response(stream, init);
182+
}
149183
// Copy into a fresh ArrayBuffer. (TS 5.7 typed Uint8Array as
150184
// Uint8Array<ArrayBufferLike>, and `.buffer` may be `SharedArrayBuffer` —
151185
// neither satisfies the DOM lib's strict ArrayBuffer-only BodyInit.)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { IncomingMessage } from "node:http";
2+
3+
/** Count complete Connect envelopes without retaining their payloads. */
4+
export function trackConnectRequest(req: IncomingMessage) {
5+
const header = Buffer.alloc(5);
6+
let headerSize = 0;
7+
let remaining = 0;
8+
let frames = 0;
9+
let closed = false;
10+
let notify = () => {};
11+
12+
const onData = (data: Buffer) => {
13+
let offset = 0;
14+
while (offset < data.length) {
15+
if (headerSize < header.length) {
16+
const size = Math.min(header.length - headerSize, data.length - offset);
17+
data.copy(header, headerSize, offset, offset + size);
18+
headerSize += size;
19+
offset += size;
20+
if (headerSize < header.length) continue;
21+
remaining = header.readUInt32BE(1);
22+
}
23+
const size = Math.min(remaining, data.length - offset);
24+
remaining -= size;
25+
offset += size;
26+
if (remaining === 0) {
27+
frames++;
28+
headerSize = 0;
29+
}
30+
}
31+
notify();
32+
};
33+
const onClose = () => {
34+
closed = true;
35+
notify();
36+
};
37+
req.on("data", onData);
38+
req.on("end", onClose);
39+
req.on("close", onClose);
40+
req.on("error", onClose);
41+
42+
return {
43+
get frames() {
44+
return frames;
45+
},
46+
// Response stream pulls are sequential, so there is only one waiter.
47+
async waitFor(count: number) {
48+
while (frames < count) {
49+
if (closed) {
50+
throw new Error(
51+
`Connect request ended after ${frames} frames; replay requires ${count}`,
52+
);
53+
}
54+
await new Promise<void>((resolve) => {
55+
notify = resolve;
56+
});
57+
}
58+
},
59+
dispose() {
60+
req.off("data", onData);
61+
req.off("end", onClose);
62+
req.off("close", onClose);
63+
req.off("error", onClose);
64+
onClose();
65+
},
66+
};
67+
}

0 commit comments

Comments
 (0)