@copilotkit/aimock 1.37.0 → 1.37.2
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/CHANGELOG.md +16 -0
- package/dist/agui-recorder.cjs +62 -0
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +23 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +23 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +60 -1
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts +13 -0
- package/dist/agui-types.d.ts +13 -0
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/journal.cjs +35 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +35 -1
- package/dist/journal.js.map +1 -1
- package/dist/recorder.cjs +12 -4
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +12 -4
- package/dist/recorder.js.map +1 -1
- package/dist/router.cjs +53 -25
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +20 -0
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +20 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +53 -25
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +4 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -1
- package/dist/server.js.map +1 -1
- package/dist/stream-collapse.cjs +120 -27
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +31 -8
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +31 -8
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +119 -28
- package/dist/stream-collapse.js.map +1 -1
- package/dist/ws-gemini-live.d.cts +2 -2
- package/dist/ws-realtime.d.cts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @copilotkit/aimock
|
|
2
2
|
|
|
3
|
+
## [1.37.2] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Prevent `Invalid string length` crash on `GET /__aimock/journal` by capping retained request bodies at 64 KB (#308)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Log full error stack on unhandled request errors for prod diagnosis (#308)
|
|
12
|
+
|
|
13
|
+
## [1.37.1] - 2026-07-16
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Capped the uncapped in-memory record buffers that could grow a string past V8's ~512 MiB max string length and throw `RangeError: Invalid string length` (the ~1/sec production crash, amplified once the real-key fixture-miss passthrough landed). The AG-UI recorder, the generic recorder, and the stream-collapse path now bound the buffer they `Buffer.concat(chunks).toString()` for fixture construction to a configurable `maxRecordBufferBytes` (default 64 MiB) clamped to a 256 MiB hard ceiling. Once the cap is crossed the recorder frees the buffer, marks the recording truncated, and skips fixture construction — the full upstream response is still relayed to the client byte-for-byte in real time, so capping never truncates what the client receives ("don't journal", not "don't answer") (#307)
|
|
18
|
+
|
|
3
19
|
## [1.37.0] - 2026-07-15
|
|
4
20
|
|
|
5
21
|
### Added
|
package/dist/agui-recorder.cjs
CHANGED
|
@@ -13,6 +13,48 @@ node_path = require_runtime.__toESM(node_path);
|
|
|
13
13
|
|
|
14
14
|
//#region src/agui-recorder.ts
|
|
15
15
|
/**
|
|
16
|
+
* Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder
|
|
17
|
+
* tees every upstream SSE chunk straight to the client AND buffers a copy so it
|
|
18
|
+
* can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture
|
|
19
|
+
* once the stream ends. With no cap, a large upstream response (amplified since
|
|
20
|
+
* the real-key fixture-miss passthrough landed) builds a string past V8's
|
|
21
|
+
* ~512 MiB max string length and throws `RangeError: Invalid string length`.
|
|
22
|
+
* 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and
|
|
23
|
+
* is generous for any real agent turn. Overridable via
|
|
24
|
+
* `AGUIRecordConfig.maxRecordBufferBytes`.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the
|
|
29
|
+
* configurable `maxRecordBufferBytes`, mirroring the generic proxy path's
|
|
30
|
+
* `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than
|
|
31
|
+
* 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,
|
|
32
|
+
* so the eventual `Buffer.concat(chunks).toString()` can never throw.
|
|
33
|
+
*/
|
|
34
|
+
const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024;
|
|
35
|
+
/**
|
|
36
|
+
* Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap
|
|
37
|
+
* suite exercise the over-cap truncation path with a small body instead of
|
|
38
|
+
* streaming hundreds of MB. `undefined` (the default) uses the configured /
|
|
39
|
+
* default cap. NEVER set from production code.
|
|
40
|
+
*/
|
|
41
|
+
let aguiRecordBufferCeilingOverride;
|
|
42
|
+
/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */
|
|
43
|
+
function setAGUIRecordBufferCeilingForTests(value) {
|
|
44
|
+
aguiRecordBufferCeilingOverride = value;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the effective AG-UI record-buffer byte cap: honor a test-only
|
|
48
|
+
* override, else the configured `maxRecordBufferBytes` (clamped to the hard
|
|
49
|
+
* ceiling), else the default. Non-finite / non-positive configured values fall
|
|
50
|
+
* back to the default.
|
|
51
|
+
*/
|
|
52
|
+
function resolveAGUIRecordBufferCap(configured) {
|
|
53
|
+
if (aguiRecordBufferCeilingOverride !== void 0) return aguiRecordBufferCeilingOverride;
|
|
54
|
+
if (configured == null || !Number.isFinite(configured) || configured <= 0) return DEFAULT_AGUI_RECORD_BUFFER_BYTES;
|
|
55
|
+
return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
16
58
|
* Proxy an unmatched AG-UI request to a real upstream agent, record the
|
|
17
59
|
* SSE event stream as a fixture on disk and in memory, and relay the
|
|
18
60
|
* response back to the original client in real time.
|
|
@@ -84,6 +126,9 @@ function teeUpstreamStream(target, headers, body, clientRes, input, fixtures, co
|
|
|
84
126
|
}
|
|
85
127
|
const chunks = [];
|
|
86
128
|
let clientWriteFailed = false;
|
|
129
|
+
const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);
|
|
130
|
+
let bufferedBytes = 0;
|
|
131
|
+
let recordTruncated = false;
|
|
87
132
|
upstreamRes.on("data", (chunk) => {
|
|
88
133
|
try {
|
|
89
134
|
clientRes.write(chunk);
|
|
@@ -93,7 +138,16 @@ function teeUpstreamStream(target, headers, body, clientRes, input, fixtures, co
|
|
|
93
138
|
logger?.warn("Client write failed during proxy relay:", err instanceof Error ? err.message : String(err));
|
|
94
139
|
}
|
|
95
140
|
}
|
|
141
|
+
if (recordTruncated) return;
|
|
142
|
+
if (bufferedBytes + chunk.length > recordBufferCap) {
|
|
143
|
+
recordTruncated = true;
|
|
144
|
+
chunks.length = 0;
|
|
145
|
+
bufferedBytes = 0;
|
|
146
|
+
logger?.warn(`AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
96
149
|
chunks.push(chunk);
|
|
150
|
+
bufferedBytes += chunk.length;
|
|
97
151
|
});
|
|
98
152
|
let settled = false;
|
|
99
153
|
upstreamRes.on("error", (err) => {
|
|
@@ -126,6 +180,11 @@ function teeUpstreamStream(target, headers, body, clientRes, input, fixtures, co
|
|
|
126
180
|
} catch (writeErr) {
|
|
127
181
|
logger.warn("Failed to end client response:", writeErr instanceof Error ? writeErr.message : String(writeErr));
|
|
128
182
|
}
|
|
183
|
+
if (recordTruncated) {
|
|
184
|
+
logger.warn("AG-UI record buffer cap exceeded — response relayed to client, recording skipped");
|
|
185
|
+
resolve(clientStatus);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
129
188
|
const events = parseSSEEvents(Buffer.concat(chunks).toString(), logger);
|
|
130
189
|
let match;
|
|
131
190
|
const lastToolResult = require_agui_handler.getLastMessageIfToolResult(input);
|
|
@@ -211,5 +270,8 @@ function parseSSEEvents(text, logger) {
|
|
|
211
270
|
}
|
|
212
271
|
|
|
213
272
|
//#endregion
|
|
273
|
+
exports.AGUI_RECORD_BUFFER_HARD_CEILING = AGUI_RECORD_BUFFER_HARD_CEILING;
|
|
274
|
+
exports.DEFAULT_AGUI_RECORD_BUFFER_BYTES = DEFAULT_AGUI_RECORD_BUFFER_BYTES;
|
|
214
275
|
exports.proxyAndRecordAGUI = proxyAndRecordAGUI;
|
|
276
|
+
exports.setAGUIRecordBufferCeilingForTests = setAGUIRecordBufferCeilingForTests;
|
|
215
277
|
//# sourceMappingURL=agui-recorder.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agui-recorder.cjs","names":["https","http","getLastMessageIfToolResult","extractLastUserMessage","crypto","path"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction\n chunks.push(chunk);\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"agui-recorder.cjs","names":["https","http","getLastMessageIfToolResult","extractLastUserMessage","crypto","path"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder\n * tees every upstream SSE chunk straight to the client AND buffers a copy so it\n * can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture\n * once the stream ends. With no cap, a large upstream response (amplified since\n * the real-key fixture-miss passthrough landed) builds a string past V8's\n * ~512 MiB max string length and throws `RangeError: Invalid string length`.\n * 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and\n * is generous for any real agent turn. Overridable via\n * `AGUIRecordConfig.maxRecordBufferBytes`.\n */\nexport const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the\n * configurable `maxRecordBufferBytes`, mirroring the generic proxy path's\n * `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than\n * 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,\n * so the eventual `Buffer.concat(chunks).toString()` can never throw.\n */\nexport const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap\n * suite exercise the over-cap truncation path with a small body instead of\n * streaming hundreds of MB. `undefined` (the default) uses the configured /\n * default cap. NEVER set from production code.\n */\nlet aguiRecordBufferCeilingOverride: number | undefined;\n\n/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */\nexport function setAGUIRecordBufferCeilingForTests(value: number | undefined): void {\n aguiRecordBufferCeilingOverride = value;\n}\n\n/**\n * Resolve the effective AG-UI record-buffer byte cap: honor a test-only\n * override, else the configured `maxRecordBufferBytes` (clamped to the hard\n * ceiling), else the default. Non-finite / non-positive configured values fall\n * back to the default.\n */\nfunction resolveAGUIRecordBufferCap(configured: number | undefined): number {\n if (aguiRecordBufferCeilingOverride !== undefined) return aguiRecordBufferCeilingOverride;\n if (configured == null || !Number.isFinite(configured) || configured <= 0) {\n return DEFAULT_AGUI_RECORD_BUFFER_BYTES;\n }\n return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);\n}\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n // Bound the in-memory record buffer so a single huge upstream response\n // cannot build a string past V8's ~512 MiB limit\n // (RangeError: Invalid string length) when we `Buffer.concat`+stringify\n // it below. The client relay above is INDEPENDENT of this buffer, so\n // capping it never truncates what the client receives — the cap means\n // \"don't journal\", not \"don't answer\" (mirrors the generic proxy path).\n const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);\n let bufferedBytes = 0;\n let recordTruncated = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction, bounded by the record-buffer cap.\n // Once the cap is crossed we stop accumulating and free the buffer so\n // heap growth is bounded and the end-handler skips concat/stringify.\n if (recordTruncated) return;\n if (bufferedBytes + chunk.length > recordBufferCap) {\n recordTruncated = true;\n chunks.length = 0;\n bufferedBytes = 0;\n logger?.warn(\n `AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`,\n );\n return;\n }\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Record buffer cap tripped: the buffered copy is partial (and was\n // freed), so we cannot faithfully build a fixture. The client already\n // received every byte via the live tee above, so just skip recording\n // — never stringify the (now-empty) buffer, never persist a truncated\n // fixture. Mirrors the generic proxy path's over-cap \"skip recording,\n // still answer the client\" behavior.\n if (recordTruncated) {\n logger.warn(\n \"AG-UI record buffer cap exceeded — response relayed to client, recording skipped\",\n );\n resolve(clientStatus);\n return;\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
|
package/dist/agui-recorder.d.cts
CHANGED
|
@@ -4,6 +4,28 @@ import * as http$1 from "node:http";
|
|
|
4
4
|
|
|
5
5
|
//#region src/agui-recorder.d.ts
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder
|
|
9
|
+
* tees every upstream SSE chunk straight to the client AND buffers a copy so it
|
|
10
|
+
* can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture
|
|
11
|
+
* once the stream ends. With no cap, a large upstream response (amplified since
|
|
12
|
+
* the real-key fixture-miss passthrough landed) builds a string past V8's
|
|
13
|
+
* ~512 MiB max string length and throws `RangeError: Invalid string length`.
|
|
14
|
+
* 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and
|
|
15
|
+
* is generous for any real agent turn. Overridable via
|
|
16
|
+
* `AGUIRecordConfig.maxRecordBufferBytes`.
|
|
17
|
+
*/
|
|
18
|
+
declare const DEFAULT_AGUI_RECORD_BUFFER_BYTES: number;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the
|
|
21
|
+
* configurable `maxRecordBufferBytes`, mirroring the generic proxy path's
|
|
22
|
+
* `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than
|
|
23
|
+
* 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,
|
|
24
|
+
* so the eventual `Buffer.concat(chunks).toString()` can never throw.
|
|
25
|
+
*/
|
|
26
|
+
declare const AGUI_RECORD_BUFFER_HARD_CEILING: number;
|
|
27
|
+
/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */
|
|
28
|
+
declare function setAGUIRecordBufferCeilingForTests(value: number | undefined): void;
|
|
7
29
|
/**
|
|
8
30
|
* Proxy an unmatched AG-UI request to a real upstream agent, record the
|
|
9
31
|
* SSE event stream as a fixture on disk and in memory, and relay the
|
|
@@ -15,5 +37,5 @@ import * as http$1 from "node:http";
|
|
|
15
37
|
declare function proxyAndRecordAGUI(req: http$1.IncomingMessage, res: http$1.ServerResponse, input: AGUIRunAgentInput, fixtures: AGUIFixture[], config: AGUIRecordConfig, logger: Logger): Promise<number | false>;
|
|
16
38
|
//# sourceMappingURL=agui-recorder.d.ts.map
|
|
17
39
|
//#endregion
|
|
18
|
-
export { proxyAndRecordAGUI };
|
|
40
|
+
export { AGUI_RECORD_BUFFER_HARD_CEILING, DEFAULT_AGUI_RECORD_BUFFER_BYTES, proxyAndRecordAGUI, setAGUIRecordBufferCeilingForTests };
|
|
19
41
|
//# sourceMappingURL=agui-recorder.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agui-recorder.d.cts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agui-recorder.d.cts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;cAiCa;;;;;;;;cASA;;iBAWG,kCAAA;;;;;;;;;iBA0BM,kBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uBACH,6BACG,uBACF,0BACA,SACP"}
|
package/dist/agui-recorder.d.ts
CHANGED
|
@@ -4,6 +4,28 @@ import * as http$1 from "node:http";
|
|
|
4
4
|
|
|
5
5
|
//#region src/agui-recorder.d.ts
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder
|
|
9
|
+
* tees every upstream SSE chunk straight to the client AND buffers a copy so it
|
|
10
|
+
* can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture
|
|
11
|
+
* once the stream ends. With no cap, a large upstream response (amplified since
|
|
12
|
+
* the real-key fixture-miss passthrough landed) builds a string past V8's
|
|
13
|
+
* ~512 MiB max string length and throws `RangeError: Invalid string length`.
|
|
14
|
+
* 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and
|
|
15
|
+
* is generous for any real agent turn. Overridable via
|
|
16
|
+
* `AGUIRecordConfig.maxRecordBufferBytes`.
|
|
17
|
+
*/
|
|
18
|
+
declare const DEFAULT_AGUI_RECORD_BUFFER_BYTES: number;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the
|
|
21
|
+
* configurable `maxRecordBufferBytes`, mirroring the generic proxy path's
|
|
22
|
+
* `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than
|
|
23
|
+
* 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,
|
|
24
|
+
* so the eventual `Buffer.concat(chunks).toString()` can never throw.
|
|
25
|
+
*/
|
|
26
|
+
declare const AGUI_RECORD_BUFFER_HARD_CEILING: number;
|
|
27
|
+
/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */
|
|
28
|
+
declare function setAGUIRecordBufferCeilingForTests(value: number | undefined): void;
|
|
7
29
|
/**
|
|
8
30
|
* Proxy an unmatched AG-UI request to a real upstream agent, record the
|
|
9
31
|
* SSE event stream as a fixture on disk and in memory, and relay the
|
|
@@ -15,5 +37,5 @@ import * as http$1 from "node:http";
|
|
|
15
37
|
declare function proxyAndRecordAGUI(req: http$1.IncomingMessage, res: http$1.ServerResponse, input: AGUIRunAgentInput, fixtures: AGUIFixture[], config: AGUIRecordConfig, logger: Logger): Promise<number | false>;
|
|
16
38
|
//# sourceMappingURL=agui-recorder.d.ts.map
|
|
17
39
|
//#endregion
|
|
18
|
-
export { proxyAndRecordAGUI };
|
|
40
|
+
export { AGUI_RECORD_BUFFER_HARD_CEILING, DEFAULT_AGUI_RECORD_BUFFER_BYTES, proxyAndRecordAGUI, setAGUIRecordBufferCeilingForTests };
|
|
19
41
|
//# sourceMappingURL=agui-recorder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agui-recorder.d.ts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agui-recorder.d.ts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;cAiCa;;;;;;;;cASA;;iBAWG,kCAAA;;;;;;;;;iBA0BM,kBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uBACH,6BACG,uBACF,0BACA,SACP"}
|
package/dist/agui-recorder.js
CHANGED
|
@@ -7,6 +7,48 @@ import * as path from "node:path";
|
|
|
7
7
|
|
|
8
8
|
//#region src/agui-recorder.ts
|
|
9
9
|
/**
|
|
10
|
+
* Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder
|
|
11
|
+
* tees every upstream SSE chunk straight to the client AND buffers a copy so it
|
|
12
|
+
* can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture
|
|
13
|
+
* once the stream ends. With no cap, a large upstream response (amplified since
|
|
14
|
+
* the real-key fixture-miss passthrough landed) builds a string past V8's
|
|
15
|
+
* ~512 MiB max string length and throws `RangeError: Invalid string length`.
|
|
16
|
+
* 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and
|
|
17
|
+
* is generous for any real agent turn. Overridable via
|
|
18
|
+
* `AGUIRecordConfig.maxRecordBufferBytes`.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
/**
|
|
22
|
+
* Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the
|
|
23
|
+
* configurable `maxRecordBufferBytes`, mirroring the generic proxy path's
|
|
24
|
+
* `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than
|
|
25
|
+
* 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,
|
|
26
|
+
* so the eventual `Buffer.concat(chunks).toString()` can never throw.
|
|
27
|
+
*/
|
|
28
|
+
const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024;
|
|
29
|
+
/**
|
|
30
|
+
* Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap
|
|
31
|
+
* suite exercise the over-cap truncation path with a small body instead of
|
|
32
|
+
* streaming hundreds of MB. `undefined` (the default) uses the configured /
|
|
33
|
+
* default cap. NEVER set from production code.
|
|
34
|
+
*/
|
|
35
|
+
let aguiRecordBufferCeilingOverride;
|
|
36
|
+
/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */
|
|
37
|
+
function setAGUIRecordBufferCeilingForTests(value) {
|
|
38
|
+
aguiRecordBufferCeilingOverride = value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the effective AG-UI record-buffer byte cap: honor a test-only
|
|
42
|
+
* override, else the configured `maxRecordBufferBytes` (clamped to the hard
|
|
43
|
+
* ceiling), else the default. Non-finite / non-positive configured values fall
|
|
44
|
+
* back to the default.
|
|
45
|
+
*/
|
|
46
|
+
function resolveAGUIRecordBufferCap(configured) {
|
|
47
|
+
if (aguiRecordBufferCeilingOverride !== void 0) return aguiRecordBufferCeilingOverride;
|
|
48
|
+
if (configured == null || !Number.isFinite(configured) || configured <= 0) return DEFAULT_AGUI_RECORD_BUFFER_BYTES;
|
|
49
|
+
return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
10
52
|
* Proxy an unmatched AG-UI request to a real upstream agent, record the
|
|
11
53
|
* SSE event stream as a fixture on disk and in memory, and relay the
|
|
12
54
|
* response back to the original client in real time.
|
|
@@ -78,6 +120,9 @@ function teeUpstreamStream(target, headers, body, clientRes, input, fixtures, co
|
|
|
78
120
|
}
|
|
79
121
|
const chunks = [];
|
|
80
122
|
let clientWriteFailed = false;
|
|
123
|
+
const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);
|
|
124
|
+
let bufferedBytes = 0;
|
|
125
|
+
let recordTruncated = false;
|
|
81
126
|
upstreamRes.on("data", (chunk) => {
|
|
82
127
|
try {
|
|
83
128
|
clientRes.write(chunk);
|
|
@@ -87,7 +132,16 @@ function teeUpstreamStream(target, headers, body, clientRes, input, fixtures, co
|
|
|
87
132
|
logger?.warn("Client write failed during proxy relay:", err instanceof Error ? err.message : String(err));
|
|
88
133
|
}
|
|
89
134
|
}
|
|
135
|
+
if (recordTruncated) return;
|
|
136
|
+
if (bufferedBytes + chunk.length > recordBufferCap) {
|
|
137
|
+
recordTruncated = true;
|
|
138
|
+
chunks.length = 0;
|
|
139
|
+
bufferedBytes = 0;
|
|
140
|
+
logger?.warn(`AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
90
143
|
chunks.push(chunk);
|
|
144
|
+
bufferedBytes += chunk.length;
|
|
91
145
|
});
|
|
92
146
|
let settled = false;
|
|
93
147
|
upstreamRes.on("error", (err) => {
|
|
@@ -120,6 +174,11 @@ function teeUpstreamStream(target, headers, body, clientRes, input, fixtures, co
|
|
|
120
174
|
} catch (writeErr) {
|
|
121
175
|
logger.warn("Failed to end client response:", writeErr instanceof Error ? writeErr.message : String(writeErr));
|
|
122
176
|
}
|
|
177
|
+
if (recordTruncated) {
|
|
178
|
+
logger.warn("AG-UI record buffer cap exceeded — response relayed to client, recording skipped");
|
|
179
|
+
resolve(clientStatus);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
123
182
|
const events = parseSSEEvents(Buffer.concat(chunks).toString(), logger);
|
|
124
183
|
let match;
|
|
125
184
|
const lastToolResult = getLastMessageIfToolResult(input);
|
|
@@ -205,5 +264,5 @@ function parseSSEEvents(text, logger) {
|
|
|
205
264
|
}
|
|
206
265
|
|
|
207
266
|
//#endregion
|
|
208
|
-
export { proxyAndRecordAGUI };
|
|
267
|
+
export { AGUI_RECORD_BUFFER_HARD_CEILING, DEFAULT_AGUI_RECORD_BUFFER_BYTES, proxyAndRecordAGUI, setAGUIRecordBufferCeilingForTests };
|
|
209
268
|
//# sourceMappingURL=agui-recorder.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agui-recorder.js","names":["http","crypto"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction\n chunks.push(chunk);\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"agui-recorder.js","names":["http","crypto"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder\n * tees every upstream SSE chunk straight to the client AND buffers a copy so it\n * can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture\n * once the stream ends. With no cap, a large upstream response (amplified since\n * the real-key fixture-miss passthrough landed) builds a string past V8's\n * ~512 MiB max string length and throws `RangeError: Invalid string length`.\n * 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and\n * is generous for any real agent turn. Overridable via\n * `AGUIRecordConfig.maxRecordBufferBytes`.\n */\nexport const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the\n * configurable `maxRecordBufferBytes`, mirroring the generic proxy path's\n * `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than\n * 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,\n * so the eventual `Buffer.concat(chunks).toString()` can never throw.\n */\nexport const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap\n * suite exercise the over-cap truncation path with a small body instead of\n * streaming hundreds of MB. `undefined` (the default) uses the configured /\n * default cap. NEVER set from production code.\n */\nlet aguiRecordBufferCeilingOverride: number | undefined;\n\n/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */\nexport function setAGUIRecordBufferCeilingForTests(value: number | undefined): void {\n aguiRecordBufferCeilingOverride = value;\n}\n\n/**\n * Resolve the effective AG-UI record-buffer byte cap: honor a test-only\n * override, else the configured `maxRecordBufferBytes` (clamped to the hard\n * ceiling), else the default. Non-finite / non-positive configured values fall\n * back to the default.\n */\nfunction resolveAGUIRecordBufferCap(configured: number | undefined): number {\n if (aguiRecordBufferCeilingOverride !== undefined) return aguiRecordBufferCeilingOverride;\n if (configured == null || !Number.isFinite(configured) || configured <= 0) {\n return DEFAULT_AGUI_RECORD_BUFFER_BYTES;\n }\n return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);\n}\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n // Bound the in-memory record buffer so a single huge upstream response\n // cannot build a string past V8's ~512 MiB limit\n // (RangeError: Invalid string length) when we `Buffer.concat`+stringify\n // it below. The client relay above is INDEPENDENT of this buffer, so\n // capping it never truncates what the client receives — the cap means\n // \"don't journal\", not \"don't answer\" (mirrors the generic proxy path).\n const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);\n let bufferedBytes = 0;\n let recordTruncated = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction, bounded by the record-buffer cap.\n // Once the cap is crossed we stop accumulating and free the buffer so\n // heap growth is bounded and the end-handler skips concat/stringify.\n if (recordTruncated) return;\n if (bufferedBytes + chunk.length > recordBufferCap) {\n recordTruncated = true;\n chunks.length = 0;\n bufferedBytes = 0;\n logger?.warn(\n `AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`,\n );\n return;\n }\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Record buffer cap tripped: the buffered copy is partial (and was\n // freed), so we cannot faithfully build a fixture. The client already\n // received every byte via the live tee above, so just skip recording\n // — never stringify the (now-empty) buffer, never persist a truncated\n // fixture. Mirrors the generic proxy path's over-cap \"skip recording,\n // still answer the client\" behavior.\n if (recordTruncated) {\n logger.warn(\n \"AG-UI record buffer cap exceeded — response relayed to client, recording skipped\",\n );\n resolve(clientStatus);\n return;\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
|
package/dist/agui-types.d.cts
CHANGED
|
@@ -259,6 +259,19 @@ interface AGUIRecordConfig {
|
|
|
259
259
|
upstream: string;
|
|
260
260
|
fixturePath?: string;
|
|
261
261
|
proxyOnly?: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Maximum number of bytes the AG-UI recorder will accumulate in memory from a
|
|
264
|
+
* single proxied upstream SSE stream in order to parse + journal it. The full
|
|
265
|
+
* stream is still relayed to the client byte-for-byte in real time; this cap
|
|
266
|
+
* only bounds the in-memory buffer that is later `Buffer.concat`-ed and
|
|
267
|
+
* stringified for fixture construction. Once exceeded the recorder stops
|
|
268
|
+
* appending to the buffer, marks the recording truncated, and skips fixture
|
|
269
|
+
* construction — preventing both unbounded heap growth and the
|
|
270
|
+
* `RangeError: Invalid string length` a >512MB `Buffer.concat(chunks).toString()`
|
|
271
|
+
* would otherwise throw. Clamped to `AGUI_RECORD_BUFFER_HARD_CEILING`.
|
|
272
|
+
* Default: 64 MiB.
|
|
273
|
+
*/
|
|
274
|
+
maxRecordBufferBytes?: number;
|
|
262
275
|
}
|
|
263
276
|
//# sourceMappingURL=agui-types.d.ts.map
|
|
264
277
|
//#endregion
|
package/dist/agui-types.d.ts
CHANGED
|
@@ -259,6 +259,19 @@ interface AGUIRecordConfig {
|
|
|
259
259
|
upstream: string;
|
|
260
260
|
fixturePath?: string;
|
|
261
261
|
proxyOnly?: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Maximum number of bytes the AG-UI recorder will accumulate in memory from a
|
|
264
|
+
* single proxied upstream SSE stream in order to parse + journal it. The full
|
|
265
|
+
* stream is still relayed to the client byte-for-byte in real time; this cap
|
|
266
|
+
* only bounds the in-memory buffer that is later `Buffer.concat`-ed and
|
|
267
|
+
* stringified for fixture construction. Once exceeded the recorder stops
|
|
268
|
+
* appending to the buffer, marks the recording truncated, and skips fixture
|
|
269
|
+
* construction — preventing both unbounded heap growth and the
|
|
270
|
+
* `RangeError: Invalid string length` a >512MB `Buffer.concat(chunks).toString()`
|
|
271
|
+
* would otherwise throw. Clamped to `AGUI_RECORD_BUFFER_HARD_CEILING`.
|
|
272
|
+
* Default: 64 MiB.
|
|
273
|
+
*/
|
|
274
|
+
maxRecordBufferBytes?: number;
|
|
262
275
|
}
|
|
263
276
|
//# sourceMappingURL=agui-types.d.ts.map
|
|
264
277
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.d.ts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAeiB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,
|
|
1
|
+
{"version":3,"file":"config-loader.d.ts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAeiB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,EAAA;EAKT,SAAA,CAAA,EAvBH,iBAuBoB,EAAA;EAOjB,OAAA,CAAA,EA7BL,eA+BC,EAAA;AAGb;AAAuC,UA/BtB,gBAAA,CA+BsB;SAMxB,EAAA,MAAA;OAHH,CAAA,EAhCF,OAgCE,EAAA;WAKK,CAAA,EApCH,WAoCG,EAAA;EAAW,MAAA,CAAA,EAnCjB,cAmCiB,EAAA;EAGX,OAAA,CAAA,EAAA,MAAY;AAK7B;AAA6B,UAvCZ,cAAA,SAAuB,kBAuCX,CAAA;UAOjB,CAAA,EA7CC,gBA6CD,EAAA;OACC,CAAA,EA7CH,gBA6CG,EAAA;gBAEL,CAAA,EA9CW,gBA8CX,EAAA;;AAEC,UA7CQ,SAAA,CA6CR;MACE,CAAA,EAAA,MAAA;EAAY,MAAA,CAAA,EA5CZ,cA4CY,EAAA;AAQvB;AAKsB,UAtDL,iBAAA,CAsDoB;EAAA,KAAA,EAAA;IAC3B,OAAA,CAAA,EAAA,MAAA;IAEW,UAAA,CAAA,EAAA,MAAA;IAAlB,QAAA,CAAA,EAAA,MAAA;IAAO,QAAA,CAAA,EAAA,MAAA;;;WAtDC;;;UAIM,UAAA;;aAEJ;;UAGI,sBAAA;;;YAGL;;;eAGG;;iBAEE;;UAGA,YAAA;;gBAED;;UAGC,YAAA;;;;;;;YAOL;aACC;;QAEL;QACA;SACC;WACE;;;;;;;;;;;iBAQK,UAAA,sBAAgC;iBAK1B,eAAA,SACZ;;;IAEP;UAAkB"}
|
package/dist/index.cjs
CHANGED
|
@@ -54,6 +54,8 @@ const require_suite = require('./suite.cjs');
|
|
|
54
54
|
|
|
55
55
|
exports.A2AMock = require_a2a_mock.A2AMock;
|
|
56
56
|
exports.AGUIMock = require_agui_mock.AGUIMock;
|
|
57
|
+
exports.AGUI_RECORD_BUFFER_HARD_CEILING = require_agui_recorder.AGUI_RECORD_BUFFER_HARD_CEILING;
|
|
58
|
+
exports.DEFAULT_AGUI_RECORD_BUFFER_BYTES = require_agui_recorder.DEFAULT_AGUI_RECORD_BUFFER_BYTES;
|
|
57
59
|
exports.DEFAULT_TEST_ID = require_constants.DEFAULT_TEST_ID;
|
|
58
60
|
exports.FORMAT_TO_CONTENT_TYPE = require_helpers.FORMAT_TO_CONTENT_TYPE;
|
|
59
61
|
exports.FalQueueStateMap = require_fal.FalQueueStateMap;
|
|
@@ -62,6 +64,7 @@ exports.GrokVideoJobMap = require_grok_video.GrokVideoJobMap;
|
|
|
62
64
|
exports.Journal = require_journal.Journal;
|
|
63
65
|
exports.LLMock = require_llmock.LLMock;
|
|
64
66
|
exports.Logger = require_logger.Logger;
|
|
67
|
+
exports.MAX_COLLAPSE_STRING_LENGTH = require_stream_collapse.MAX_COLLAPSE_STRING_LENGTH;
|
|
65
68
|
exports.MCPMock = require_mcp_mock.MCPMock;
|
|
66
69
|
exports.OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES = require_openrouter_video.OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES;
|
|
67
70
|
exports.OPENROUTER_VIDEO_MAX_ENTRIES = require_openrouter_video.OPENROUTER_VIDEO_MAX_ENTRIES;
|
|
@@ -190,6 +193,8 @@ exports.proxyAndRecord = require_recorder.proxyAndRecord;
|
|
|
190
193
|
exports.proxyAndRecordAGUI = require_agui_recorder.proxyAndRecordAGUI;
|
|
191
194
|
exports.resolveFixtureBlocks = require_helpers.resolveFixtureBlocks;
|
|
192
195
|
exports.resolveUpstreamUrl = require_url.resolveUpstreamUrl;
|
|
196
|
+
exports.setAGUIRecordBufferCeilingForTests = require_agui_recorder.setAGUIRecordBufferCeilingForTests;
|
|
197
|
+
exports.setCollapseStringLimitForTests = require_stream_collapse.setCollapseStringLimitForTests;
|
|
193
198
|
exports.startFromConfig = require_config_loader.startFromConfig;
|
|
194
199
|
exports.strictNoMatchLogLine = require_helpers.strictNoMatchLogLine;
|
|
195
200
|
exports.strictNoMatchMessage = require_helpers.strictNoMatchMessage;
|