@dbx-tools/appkit-mastra 0.1.41 → 0.1.42

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.
@@ -1,167 +0,0 @@
1
- /**
2
- * Server-side Server-Sent-Events frame interceptor for the native
3
- * Mastra agent `/stream` endpoint.
4
- *
5
- * The Mastra stream (`@mastra/express`) emits one `data: <json>\n\n`
6
- * SSE frame per chunk, where the decoded JSON carries a
7
- * discriminating `type` (`text-delta`, `reasoning-delta`,
8
- * `tool-call`, `step-finish`, ...) and is terminated by a
9
- * `data: [DONE]` sentinel. {@link installStreamEventInterceptor} wraps
10
- * an Express response and runs a caller-supplied
11
- * {@link StreamFrameInterceptor} over each decoded frame so the caller
12
- * can keep, rewrite, or drop it; every non-`data:` byte (SSE
13
- * keep-alive comments, the `[DONE]` sentinel, blank padding) passes
14
- * through verbatim.
15
- *
16
- * The wrap is inert unless the response turns out to be a streamed
17
- * `200 text/event-stream` body, so non-streaming JSON routes and
18
- * error responses are never touched.
19
- */
20
- import { StringDecoder } from "node:string_decoder";
21
- /** SSE event separator used by the Mastra stream wire format. */
22
- const FRAME_DELIMITER = "\n\n";
23
- /**
24
- * Matches a whole SSE frame whose `data:` payload is a JSON object,
25
- * capturing it in three parts: the `data:` field prefix (including
26
- * whatever inter-token whitespace the producer used), the JSON object
27
- * body, and any trailing whitespace. Anchored end-to-end so it only
28
- * matches a single self-contained object frame; anything else (SSE
29
- * comments, the `[DONE]` sentinel, text/array payloads) fails the
30
- * match and is forwarded verbatim without a parse attempt. On rewrite
31
- * the captured prefix/suffix are re-emitted as-is so only the body
32
- * changes.
33
- */
34
- const DATA_OBJECT_RE = /^(data:\s*)(\{[\s\S]*\})(\s*)$/;
35
- /**
36
- * True when `res` is a `200 text/event-stream` body - the only
37
- * response shape we intercept. Content-type based so it tracks the
38
- * Mastra `/stream` route without hard-coding the path, and naturally
39
- * skips the JSON routes (history, models, charts, statements) and any
40
- * non-200 error response.
41
- */
42
- function isEventStream(res) {
43
- if (res.statusCode !== 200)
44
- return false;
45
- const contentType = String(res.getHeader("content-type") ?? "").toLowerCase();
46
- return contentType.includes("text/event-stream");
47
- }
48
- /**
49
- * Run `interceptor` over a single SSE frame. Forwards anything that
50
- * isn't a parseable `data: {json}` object frame verbatim (comments,
51
- * the `[DONE]` sentinel, blank padding, unparseable / non-object
52
- * payloads) so the interceptor only ever sees real chunk objects.
53
- * Returns the frame string to emit, or `undefined` to drop the frame.
54
- */
55
- function interceptFrame(frame, interceptor) {
56
- const match = DATA_OBJECT_RE.exec(frame);
57
- if (!match)
58
- return frame;
59
- const [, prefix, body, suffix] = match;
60
- let parsed;
61
- try {
62
- parsed = JSON.parse(body);
63
- }
64
- catch {
65
- // Forward unparseable payloads untouched - feeding a partial /
66
- // malformed frame to the interceptor risks corrupting bytes the
67
- // client could still have handled.
68
- return frame;
69
- }
70
- const result = interceptor(parsed);
71
- if (result === true)
72
- return frame;
73
- if (result === false)
74
- return undefined;
75
- // Preserve the exact captured prefix/suffix so only the JSON body
76
- // changes; the framing the client parses stays byte-identical.
77
- return `${prefix}${JSON.stringify(result.replace)}${suffix}`;
78
- }
79
- /**
80
- * Wrap `res.write` / `res.end` so each SSE frame on a streamed Mastra
81
- * response is run through `interceptor` (keep / rewrite / drop).
82
- *
83
- * Interception only engages once the response is confirmed to be a
84
- * `200 text/event-stream` body (decided lazily on the first write,
85
- * after the handler has set status + content-type); any other
86
- * response streams through unchanged. A {@link StringDecoder}
87
- * reassembles multi-byte UTF-8 sequences that straddle chunk
88
- * boundaries, and a running buffer holds the trailing partial frame
89
- * until its `\n\n` arrives.
90
- */
91
- export function installStreamEventInterceptor(res, interceptor) {
92
- const origWrite = res.write.bind(res);
93
- const origEnd = res.end.bind(res);
94
- const decoder = new StringDecoder("utf8");
95
- let engaged;
96
- let buffer = "";
97
- // Append decoded text, emit every whole `\n\n`-delimited frame the
98
- // interceptor keeps, and retain any trailing partial frame in
99
- // `buffer`. On `final`, the residual buffer is flushed as the last
100
- // frame so a stream that doesn't end on a delimiter isn't dropped.
101
- const intercept = (text, final) => {
102
- buffer += text;
103
- const out = [];
104
- let idx;
105
- while ((idx = buffer.indexOf(FRAME_DELIMITER)) !== -1) {
106
- const frame = buffer.slice(0, idx);
107
- buffer = buffer.slice(idx + FRAME_DELIMITER.length);
108
- const kept = interceptFrame(frame, interceptor);
109
- if (kept !== undefined) {
110
- out.push(kept, FRAME_DELIMITER);
111
- }
112
- }
113
- if (final && buffer.length > 0) {
114
- const kept = interceptFrame(buffer, interceptor);
115
- if (kept !== undefined)
116
- out.push(kept);
117
- buffer = "";
118
- }
119
- return out.length === 0 ? "" : out.join("");
120
- };
121
- const toText = (chunk) => {
122
- if (typeof chunk === "string")
123
- return chunk;
124
- if (Buffer.isBuffer(chunk))
125
- return decoder.write(chunk);
126
- if (chunk instanceof Uint8Array) {
127
- return decoder.write(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength));
128
- }
129
- return "";
130
- };
131
- res.write = function patchedWrite(chunk, encodingOrCb, cb) {
132
- if (engaged === undefined)
133
- engaged = isEventStream(res);
134
- if (!engaged) {
135
- return origWrite(chunk, encodingOrCb, cb);
136
- }
137
- const callback = typeof encodingOrCb === "function" ? encodingOrCb : cb;
138
- const out = intercept(toText(chunk), false);
139
- if (out.length === 0) {
140
- // The chunk only advanced a partial frame; nothing to forward
141
- // yet. Honor the write callback so backpressure-aware writers
142
- // don't stall waiting on an ack that never comes.
143
- if (callback)
144
- queueMicrotask(() => callback());
145
- return true;
146
- }
147
- return origWrite(out, callback);
148
- };
149
- res.end = function patchedEnd(chunk, encodingOrCb, cb) {
150
- if (engaged === undefined)
151
- engaged = isEventStream(res);
152
- if (!engaged) {
153
- return origEnd(chunk, encodingOrCb, cb);
154
- }
155
- const callback = typeof chunk === "function"
156
- ? chunk
157
- : typeof encodingOrCb === "function"
158
- ? encodingOrCb
159
- : cb;
160
- const text = (typeof chunk !== "function" && chunk !== undefined ? toText(chunk) : "") +
161
- decoder.end();
162
- const out = intercept(text, true);
163
- if (out.length > 0)
164
- origWrite(out);
165
- return origEnd(callback);
166
- };
167
- }
package/src/intercept.ts DELETED
@@ -1,206 +0,0 @@
1
- /**
2
- * Server-side Server-Sent-Events frame interceptor for the native
3
- * Mastra agent `/stream` endpoint.
4
- *
5
- * The Mastra stream (`@mastra/express`) emits one `data: <json>\n\n`
6
- * SSE frame per chunk, where the decoded JSON carries a
7
- * discriminating `type` (`text-delta`, `reasoning-delta`,
8
- * `tool-call`, `step-finish`, ...) and is terminated by a
9
- * `data: [DONE]` sentinel. {@link installStreamEventInterceptor} wraps
10
- * an Express response and runs a caller-supplied
11
- * {@link StreamFrameInterceptor} over each decoded frame so the caller
12
- * can keep, rewrite, or drop it; every non-`data:` byte (SSE
13
- * keep-alive comments, the `[DONE]` sentinel, blank padding) passes
14
- * through verbatim.
15
- *
16
- * The wrap is inert unless the response turns out to be a streamed
17
- * `200 text/event-stream` body, so non-streaming JSON routes and
18
- * error responses are never touched.
19
- */
20
-
21
- import { StringDecoder } from "node:string_decoder";
22
-
23
- import type express from "express";
24
-
25
- /** SSE event separator used by the Mastra stream wire format. */
26
- const FRAME_DELIMITER = "\n\n";
27
-
28
- /**
29
- * Matches a whole SSE frame whose `data:` payload is a JSON object,
30
- * capturing it in three parts: the `data:` field prefix (including
31
- * whatever inter-token whitespace the producer used), the JSON object
32
- * body, and any trailing whitespace. Anchored end-to-end so it only
33
- * matches a single self-contained object frame; anything else (SSE
34
- * comments, the `[DONE]` sentinel, text/array payloads) fails the
35
- * match and is forwarded verbatim without a parse attempt. On rewrite
36
- * the captured prefix/suffix are re-emitted as-is so only the body
37
- * changes.
38
- */
39
- const DATA_OBJECT_RE = /^(data:\s*)(\{[\s\S]*\})(\s*)$/;
40
-
41
- /**
42
- * Per-frame interceptor invoked with the `JSON.parse`d object of every
43
- * `data: {json}` frame on the Mastra `/stream` response. The return
44
- * value decides the frame's fate:
45
- *
46
- * - `true`: forward the frame's original bytes unchanged (no
47
- * re-serialization).
48
- * - `false`: drop the frame entirely.
49
- * - `{ replace }`: re-emit the frame with the original `data:` prefix
50
- * and trailing whitespace preserved and only the JSON body replaced
51
- * by `JSON.stringify(replace)`.
52
- */
53
- export type StreamFrameInterceptor = (
54
- chunk: unknown,
55
- ) => { replace: unknown } | true | false;
56
-
57
- /**
58
- * True when `res` is a `200 text/event-stream` body - the only
59
- * response shape we intercept. Content-type based so it tracks the
60
- * Mastra `/stream` route without hard-coding the path, and naturally
61
- * skips the JSON routes (history, models, charts, statements) and any
62
- * non-200 error response.
63
- */
64
- function isEventStream(res: express.Response): boolean {
65
- if (res.statusCode !== 200) return false;
66
- const contentType = String(res.getHeader("content-type") ?? "").toLowerCase();
67
- return contentType.includes("text/event-stream");
68
- }
69
-
70
- /**
71
- * Run `interceptor` over a single SSE frame. Forwards anything that
72
- * isn't a parseable `data: {json}` object frame verbatim (comments,
73
- * the `[DONE]` sentinel, blank padding, unparseable / non-object
74
- * payloads) so the interceptor only ever sees real chunk objects.
75
- * Returns the frame string to emit, or `undefined` to drop the frame.
76
- */
77
- function interceptFrame(
78
- frame: string,
79
- interceptor: StreamFrameInterceptor,
80
- ): string | undefined {
81
- const match = DATA_OBJECT_RE.exec(frame);
82
- if (!match) return frame;
83
- const [, prefix, body, suffix] = match;
84
- let parsed: unknown;
85
- try {
86
- parsed = JSON.parse(body!);
87
- } catch {
88
- // Forward unparseable payloads untouched - feeding a partial /
89
- // malformed frame to the interceptor risks corrupting bytes the
90
- // client could still have handled.
91
- return frame;
92
- }
93
- const result = interceptor(parsed);
94
- if (result === true) return frame;
95
- if (result === false) return undefined;
96
- // Preserve the exact captured prefix/suffix so only the JSON body
97
- // changes; the framing the client parses stays byte-identical.
98
- return `${prefix}${JSON.stringify(result.replace)}${suffix}`;
99
- }
100
-
101
- /**
102
- * Wrap `res.write` / `res.end` so each SSE frame on a streamed Mastra
103
- * response is run through `interceptor` (keep / rewrite / drop).
104
- *
105
- * Interception only engages once the response is confirmed to be a
106
- * `200 text/event-stream` body (decided lazily on the first write,
107
- * after the handler has set status + content-type); any other
108
- * response streams through unchanged. A {@link StringDecoder}
109
- * reassembles multi-byte UTF-8 sequences that straddle chunk
110
- * boundaries, and a running buffer holds the trailing partial frame
111
- * until its `\n\n` arrives.
112
- */
113
- export function installStreamEventInterceptor(
114
- res: express.Response,
115
- interceptor: StreamFrameInterceptor,
116
- ): void {
117
- const origWrite = res.write.bind(res);
118
- const origEnd = res.end.bind(res);
119
- const decoder = new StringDecoder("utf8");
120
- let engaged: boolean | undefined;
121
- let buffer = "";
122
-
123
- // Append decoded text, emit every whole `\n\n`-delimited frame the
124
- // interceptor keeps, and retain any trailing partial frame in
125
- // `buffer`. On `final`, the residual buffer is flushed as the last
126
- // frame so a stream that doesn't end on a delimiter isn't dropped.
127
- const intercept = (text: string, final: boolean): string => {
128
- buffer += text;
129
-
130
- const out: string[] = [];
131
-
132
- let idx: number;
133
- while ((idx = buffer.indexOf(FRAME_DELIMITER)) !== -1) {
134
- const frame = buffer.slice(0, idx);
135
- buffer = buffer.slice(idx + FRAME_DELIMITER.length);
136
-
137
- const kept = interceptFrame(frame, interceptor);
138
- if (kept !== undefined) {
139
- out.push(kept, FRAME_DELIMITER);
140
- }
141
- }
142
-
143
- if (final && buffer.length > 0) {
144
- const kept = interceptFrame(buffer, interceptor);
145
- if (kept !== undefined) out.push(kept);
146
- buffer = "";
147
- }
148
-
149
- return out.length === 0 ? "" : out.join("");
150
- };
151
-
152
- const toText = (chunk: unknown): string => {
153
- if (typeof chunk === "string") return chunk;
154
- if (Buffer.isBuffer(chunk)) return decoder.write(chunk);
155
- if (chunk instanceof Uint8Array) {
156
- return decoder.write(
157
- Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength),
158
- );
159
- }
160
- return "";
161
- };
162
-
163
- res.write = function patchedWrite(
164
- chunk: unknown,
165
- encodingOrCb?: BufferEncoding | ((error?: Error | null) => void),
166
- cb?: (error?: Error | null) => void,
167
- ): boolean {
168
- if (engaged === undefined) engaged = isEventStream(res);
169
- if (!engaged) {
170
- return origWrite(chunk as never, encodingOrCb as never, cb as never);
171
- }
172
- const callback = typeof encodingOrCb === "function" ? encodingOrCb : cb;
173
- const out = intercept(toText(chunk), false);
174
- if (out.length === 0) {
175
- // The chunk only advanced a partial frame; nothing to forward
176
- // yet. Honor the write callback so backpressure-aware writers
177
- // don't stall waiting on an ack that never comes.
178
- if (callback) queueMicrotask(() => callback());
179
- return true;
180
- }
181
- return origWrite(out, callback as never);
182
- } as typeof res.write;
183
-
184
- res.end = function patchedEnd(
185
- chunk?: unknown | (() => void),
186
- encodingOrCb?: BufferEncoding | (() => void),
187
- cb?: () => void,
188
- ): express.Response {
189
- if (engaged === undefined) engaged = isEventStream(res);
190
- if (!engaged) {
191
- return origEnd(chunk as never, encodingOrCb as never, cb as never);
192
- }
193
- const callback =
194
- typeof chunk === "function"
195
- ? chunk
196
- : typeof encodingOrCb === "function"
197
- ? encodingOrCb
198
- : cb;
199
- const text =
200
- (typeof chunk !== "function" && chunk !== undefined ? toText(chunk) : "") +
201
- decoder.end();
202
- const out = intercept(text, true);
203
- if (out.length > 0) origWrite(out);
204
- return origEnd(callback as never);
205
- } as typeof res.end;
206
- }