@ai-matrx/agents 0.2.1 → 0.5.0

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,11 +1,17 @@
1
1
  // stream/ndjson.ts
2
+ var DEFAULT_MATRX_NDJSON_READ_AHEAD = 64;
2
3
  function isRecord(value) {
3
4
  return typeof value === "object" && value !== null && !Array.isArray(value);
4
5
  }
5
6
  function normalizeMatrxStreamEnvelope(value) {
6
7
  if (!isRecord(value)) return null;
7
8
  if (typeof value.event === "string") {
8
- return { event: value.event, data: value.data };
9
+ const streamSeq = typeof value.stream_seq === "number" && Number.isFinite(value.stream_seq) ? value.stream_seq : void 0;
10
+ return {
11
+ event: value.event,
12
+ data: value.data,
13
+ ...streamSeq === void 0 ? {} : { stream_seq: streamSeq }
14
+ };
9
15
  }
10
16
  if (value.e === "c" && typeof value.t === "string") {
11
17
  return { event: "chunk", data: { text: value.t } };
@@ -15,74 +21,179 @@ function normalizeMatrxStreamEnvelope(value) {
15
21
  }
16
22
  return null;
17
23
  }
18
- async function* readMatrxNdjsonStream(body, options = {}) {
19
- const queue = [];
20
- let wakeConsumer = null;
21
- let readerFinished = false;
22
- const enqueue = (item) => {
23
- queue.push(item);
24
- const wake = wakeConsumer;
25
- wakeConsumer = null;
26
- wake?.();
27
- };
28
- const reader = body.getReader();
24
+ function createMatrxNdjsonFramer(options = {}) {
29
25
  const decoder = new TextDecoder();
30
- const parseLine = (line) => {
26
+ let buffer = "";
27
+ let lineNumber = 0;
28
+ let finished = false;
29
+ const assertOpen = () => {
30
+ if (finished) throw new Error("Matrx NDJSON framer is already finished");
31
+ };
32
+ const parseLine = (line, atCompletion) => {
33
+ const currentLineNumber = ++lineNumber;
31
34
  const trimmed = line.trim();
32
- if (!trimmed) return;
35
+ if (!trimmed) return null;
33
36
  let parsed;
34
37
  try {
35
38
  parsed = JSON.parse(trimmed);
36
39
  } catch (error) {
37
- options.onMalformedLine?.({ line: trimmed, error });
38
- return;
40
+ options.onMalformedLine?.({
41
+ line: trimmed,
42
+ error,
43
+ lineNumber: currentLineNumber,
44
+ atCompletion
45
+ });
46
+ return null;
39
47
  }
40
48
  const envelope = normalizeMatrxStreamEnvelope(parsed);
41
- if (envelope) {
42
- enqueue({ kind: "event", value: envelope });
43
- } else {
49
+ if (!envelope) {
44
50
  options.onUnknownEnvelope?.(parsed);
51
+ return null;
52
+ }
53
+ options.onValidEnvelope?.({
54
+ raw: parsed,
55
+ envelope,
56
+ line: trimmed,
57
+ lineNumber: currentLineNumber,
58
+ atCompletion
59
+ });
60
+ return envelope;
61
+ };
62
+ const pushDecodedText = (fragment) => {
63
+ buffer += fragment;
64
+ const lines = buffer.split("\n");
65
+ buffer = lines.pop() ?? "";
66
+ const envelopes = [];
67
+ for (const line of lines) {
68
+ const envelope = parseLine(line, false);
69
+ if (envelope) envelopes.push(envelope);
45
70
  }
71
+ return envelopes;
46
72
  };
73
+ return {
74
+ pushText(fragment) {
75
+ assertOpen();
76
+ return pushDecodedText(decoder.decode() + fragment);
77
+ },
78
+ pushBytes(fragment) {
79
+ assertOpen();
80
+ return pushDecodedText(decoder.decode(fragment, { stream: true }));
81
+ },
82
+ finish() {
83
+ assertOpen();
84
+ finished = true;
85
+ const envelopes = pushDecodedText(decoder.decode());
86
+ if (buffer.length > 0) {
87
+ const envelope = parseLine(buffer, true);
88
+ buffer = "";
89
+ if (envelope) envelopes.push(envelope);
90
+ }
91
+ return envelopes;
92
+ }
93
+ };
94
+ }
95
+ function readAheadLimit(value) {
96
+ const limit = value ?? DEFAULT_MATRX_NDJSON_READ_AHEAD;
97
+ if (!Number.isSafeInteger(limit) || limit < 1) {
98
+ throw new RangeError("maxReadAhead must be a positive safe integer");
99
+ }
100
+ return limit;
101
+ }
102
+ async function* readMatrxNdjsonStream(body, options = {}) {
103
+ const maxReadAhead = readAheadLimit(options.maxReadAhead);
104
+ const queue = [];
105
+ let queuedEventCount = 0;
106
+ let wakeConsumer = null;
107
+ let wakeProducer = null;
108
+ let readerFinished = false;
109
+ let consumerClosed = false;
110
+ const wakeWaitingConsumer = () => {
111
+ const wake = wakeConsumer;
112
+ wakeConsumer = null;
113
+ wake?.();
114
+ };
115
+ const wakeWaitingProducer = () => {
116
+ const wake = wakeProducer;
117
+ wakeProducer = null;
118
+ wake?.();
119
+ };
120
+ const enqueueTerminal = (item) => {
121
+ if (consumerClosed) return;
122
+ queue.push(item);
123
+ wakeWaitingConsumer();
124
+ };
125
+ const enqueueEvent = async (value) => {
126
+ while (queuedEventCount >= maxReadAhead && !consumerClosed && !options.signal?.aborted) {
127
+ await new Promise((resolve) => {
128
+ wakeProducer = resolve;
129
+ });
130
+ }
131
+ if (consumerClosed || options.signal?.aborted) return false;
132
+ queue.push({ kind: "event", value });
133
+ queuedEventCount += 1;
134
+ wakeWaitingConsumer();
135
+ return true;
136
+ };
137
+ const waitForReadCapacity = async () => {
138
+ while (queuedEventCount >= maxReadAhead && !consumerClosed && !options.signal?.aborted) {
139
+ await new Promise((resolve) => {
140
+ wakeProducer = resolve;
141
+ });
142
+ }
143
+ return !consumerClosed && !options.signal?.aborted;
144
+ };
145
+ const reader = body.getReader();
146
+ const framer = createMatrxNdjsonFramer(options);
47
147
  const onAbort = () => {
148
+ wakeWaitingProducer();
149
+ wakeWaitingConsumer();
48
150
  void reader.cancel(options.signal?.reason).catch(() => void 0);
49
151
  };
50
152
  options.signal?.addEventListener("abort", onAbort, { once: true });
153
+ if (options.signal?.aborted) onAbort();
51
154
  const readerPromise = (async () => {
52
- let buffer = "";
53
155
  try {
54
- while (!options.signal?.aborted) {
156
+ while (!options.signal?.aborted && !consumerClosed) {
157
+ if (!await waitForReadCapacity()) return;
55
158
  const { value, done } = await reader.read();
56
159
  if (done) break;
57
- buffer += decoder.decode(value, { stream: true });
58
- const lines = buffer.split("\n");
59
- buffer = lines.pop() ?? "";
60
- for (const line of lines) parseLine(line);
160
+ for (const envelope of framer.pushBytes(value)) {
161
+ if (!await enqueueEvent(envelope)) return;
162
+ }
163
+ }
164
+ if (!options.signal?.aborted && !consumerClosed) {
165
+ for (const envelope of framer.finish()) {
166
+ if (!await enqueueEvent(envelope)) return;
167
+ }
61
168
  }
62
- buffer += decoder.decode();
63
- if (!options.signal?.aborted && buffer.trim()) parseLine(buffer);
64
169
  } catch (error) {
65
- const aborted = options.signal?.aborted || error instanceof Error && error.name === "AbortError";
66
- if (!aborted) enqueue({ kind: "error", error });
170
+ const aborted = options.signal?.aborted || consumerClosed || error instanceof Error && error.name === "AbortError";
171
+ if (!aborted) enqueueTerminal({ kind: "error", error });
67
172
  } finally {
68
173
  readerFinished = true;
69
174
  reader.releaseLock();
70
- enqueue({ kind: "done" });
175
+ enqueueTerminal({ kind: "done" });
71
176
  }
72
177
  })();
73
178
  try {
74
179
  while (true) {
75
180
  if (queue.length === 0) {
181
+ if (options.signal?.aborted || readerFinished) return;
76
182
  await new Promise((resolve) => {
77
183
  wakeConsumer = resolve;
78
184
  });
79
185
  }
80
186
  const item = queue.shift();
187
+ if (item?.kind === "event") queuedEventCount -= 1;
188
+ wakeWaitingProducer();
81
189
  if (!item || item.kind === "done") return;
82
190
  if (item.kind === "error") throw item.error;
83
191
  yield item.value;
84
192
  }
85
193
  } finally {
194
+ consumerClosed = true;
195
+ wakeWaitingProducer();
196
+ wakeWaitingConsumer();
86
197
  options.signal?.removeEventListener("abort", onAbort);
87
198
  if (!readerFinished) {
88
199
  await reader.cancel().catch(() => void 0);
@@ -91,6 +202,6 @@ async function* readMatrxNdjsonStream(body, options = {}) {
91
202
  }
92
203
  }
93
204
 
94
- export { normalizeMatrxStreamEnvelope, readMatrxNdjsonStream };
205
+ export { DEFAULT_MATRX_NDJSON_READ_AHEAD, createMatrxNdjsonFramer, normalizeMatrxStreamEnvelope, readMatrxNdjsonStream };
95
206
  //# sourceMappingURL=ndjson.js.map
96
207
  //# sourceMappingURL=ndjson.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../stream/ndjson.ts"],"names":[],"mappings":"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","file":"ndjson.js","sourcesContent":["/**\n * Canonical AI Matrx NDJSON wire kernel.\n *\n * This module is deliberately independent of React, Redux, Next.js, Supabase,\n * and generated application types. Every Matrx client uses it to turn the\n * backend's byte stream into the same normalized `{ event, data }` envelopes.\n * Host runtimes remain responsible for HTTP/auth errors and for deciding what\n * each event means in their state model.\n */\n\nexport interface MatrxStreamEnvelope<TData = unknown> {\n event: string;\n data: TData;\n}\n\nexport interface MatrxNdjsonIssue {\n line: string;\n error: unknown;\n}\n\nexport interface ReadMatrxNdjsonOptions {\n signal?: AbortSignal;\n /** Malformed JSON is non-fatal, but it must never disappear silently. */\n onMalformedLine?: (issue: MatrxNdjsonIssue) => void;\n /** Valid JSON with no recognized Matrx event envelope is also non-fatal. */\n onUnknownEnvelope?: (value: unknown) => void;\n}\n\ntype QueueItem =\n | { kind: \"event\"; value: MatrxStreamEnvelope }\n | { kind: \"error\"; error: unknown }\n | { kind: \"done\" };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Normalize both supported Matrx wire shapes:\n *\n * - full: `{ \"event\": \"chunk\", \"data\": { \"text\": \"...\" } }`\n * - compact chunk: `{ \"e\": \"c\", \"t\": \"...\" }`\n * - compact reasoning: `{ \"e\": \"r\", \"t\": \"...\" }`\n */\nexport function normalizeMatrxStreamEnvelope(\n value: unknown,\n): MatrxStreamEnvelope | null {\n if (!isRecord(value)) return null;\n\n if (typeof value.event === \"string\") {\n return { event: value.event, data: value.data };\n }\n if (value.e === \"c\" && typeof value.t === \"string\") {\n return { event: \"chunk\", data: { text: value.t } };\n }\n if (value.e === \"r\" && typeof value.t === \"string\") {\n return { event: \"reasoning_chunk\", data: { text: value.t } };\n }\n return null;\n}\n\n/**\n * Read and normalize a Matrx NDJSON response body without applying consumer\n * backpressure to the network reader. The background read-ahead is important:\n * large tool payloads must keep draining even while React or another host is\n * processing the previous event.\n */\nexport async function* readMatrxNdjsonStream(\n body: ReadableStream<Uint8Array>,\n options: ReadMatrxNdjsonOptions = {},\n): AsyncGenerator<MatrxStreamEnvelope, void, undefined> {\n const queue: QueueItem[] = [];\n let wakeConsumer: (() => void) | null = null;\n let readerFinished = false;\n\n const enqueue = (item: QueueItem): void => {\n queue.push(item);\n const wake = wakeConsumer;\n wakeConsumer = null;\n wake?.();\n };\n\n const reader = body.getReader();\n const decoder = new TextDecoder();\n\n const parseLine = (line: string): void => {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed) as unknown;\n } catch (error) {\n options.onMalformedLine?.({ line: trimmed, error });\n return;\n }\n\n const envelope = normalizeMatrxStreamEnvelope(parsed);\n if (envelope) {\n enqueue({ kind: \"event\", value: envelope });\n } else {\n options.onUnknownEnvelope?.(parsed);\n }\n };\n\n const onAbort = (): void => {\n void reader.cancel(options.signal?.reason).catch(() => undefined);\n };\n options.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n const readerPromise = (async (): Promise<void> => {\n let buffer = \"\";\n try {\n while (!options.signal?.aborted) {\n const { value, done } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n for (const line of lines) parseLine(line);\n }\n\n buffer += decoder.decode();\n if (!options.signal?.aborted && buffer.trim()) parseLine(buffer);\n } catch (error) {\n const aborted =\n options.signal?.aborted ||\n (error instanceof Error && error.name === \"AbortError\");\n if (!aborted) enqueue({ kind: \"error\", error });\n } finally {\n readerFinished = true;\n reader.releaseLock();\n enqueue({ kind: \"done\" });\n }\n })();\n\n try {\n while (true) {\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wakeConsumer = resolve;\n });\n }\n\n const item = queue.shift();\n if (!item || item.kind === \"done\") return;\n if (item.kind === \"error\") throw item.error;\n yield item.value;\n }\n } finally {\n options.signal?.removeEventListener(\"abort\", onAbort);\n if (!readerFinished) {\n await reader.cancel().catch(() => undefined);\n }\n await readerPromise;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../stream/ndjson.ts"],"names":[],"mappings":"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","file":"ndjson.js","sourcesContent":["/**\n * Canonical AI Matrx NDJSON wire kernel.\n *\n * This module is deliberately independent of React, Redux, Next.js, Supabase,\n * and generated application types. Every Matrx client uses it to turn the\n * backend's byte stream into the same normalized `{ event, data }` envelopes.\n * Host runtimes remain responsible for HTTP/auth errors and for deciding what\n * each event means in their state model.\n */\n\nexport interface MatrxStreamEnvelope<TData = unknown> {\n event: string;\n data: TData;\n /** Monotonic transport sequence from full envelopes, when supplied. */\n stream_seq?: number;\n}\n\nexport interface MatrxNdjsonIssue {\n line: string;\n error: unknown;\n /** One-based physical NDJSON line number. */\n lineNumber: number;\n /** True when an unterminated trailing fragment was parsed by `finish()`. */\n atCompletion: boolean;\n}\n\nexport interface MatrxStreamEnvelopeObservation {\n /** Exact parsed JSON value before compact/full normalization. */\n raw: unknown;\n envelope: MatrxStreamEnvelope;\n line: string;\n lineNumber: number;\n atCompletion: boolean;\n}\n\nexport interface MatrxNdjsonFramerOptions {\n /** Malformed JSON is non-fatal, but it must never disappear silently. */\n onMalformedLine?: (issue: MatrxNdjsonIssue) => void;\n /** Valid JSON with no recognized Matrx event envelope is also non-fatal. */\n onUnknownEnvelope?: (value: unknown) => void;\n /** Observe every valid envelope without changing or consuming it. */\n onValidEnvelope?: (observation: MatrxStreamEnvelopeObservation) => void;\n}\n\nexport interface ReadMatrxNdjsonOptions extends MatrxNdjsonFramerOptions {\n signal?: AbortSignal;\n /** Maximum normalized events read ahead of the iterator consumer. */\n maxReadAhead?: number;\n}\n\nexport interface MatrxNdjsonFramer {\n /** Push a text fragment. Fragments may split JSON tokens or line endings. */\n pushText(fragment: string): MatrxStreamEnvelope[];\n /** Push a byte fragment. UTF-8 code points may span calls. */\n pushBytes(fragment: Uint8Array): MatrxStreamEnvelope[];\n /** Parse the final unterminated line and flush any pending UTF-8 bytes. */\n finish(): MatrxStreamEnvelope[];\n}\n\ntype QueueItem =\n | { kind: \"event\"; value: MatrxStreamEnvelope }\n | { kind: \"error\"; error: unknown }\n | { kind: \"done\" };\n\nexport const DEFAULT_MATRX_NDJSON_READ_AHEAD = 64;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Normalize both supported Matrx wire shapes:\n *\n * - full: `{ \"event\": \"chunk\", \"data\": { \"text\": \"...\" } }`\n * - compact chunk: `{ \"e\": \"c\", \"t\": \"...\" }`\n * - compact reasoning: `{ \"e\": \"r\", \"t\": \"...\" }`\n */\nexport function normalizeMatrxStreamEnvelope(\n value: unknown,\n): MatrxStreamEnvelope | null {\n if (!isRecord(value)) return null;\n\n if (typeof value.event === \"string\") {\n const streamSeq =\n typeof value.stream_seq === \"number\" && Number.isFinite(value.stream_seq)\n ? value.stream_seq\n : undefined;\n return {\n event: value.event,\n data: value.data,\n ...(streamSeq === undefined ? {} : { stream_seq: streamSeq }),\n };\n }\n if (value.e === \"c\" && typeof value.t === \"string\") {\n return { event: \"chunk\", data: { text: value.t } };\n }\n if (value.e === \"r\" && typeof value.t === \"string\") {\n return { event: \"reasoning_chunk\", data: { text: value.t } };\n }\n return null;\n}\n\n/**\n * Create the transport-independent NDJSON framer used by the stream reader.\n * Browser extensions, desktop bridges, WebSockets, and tests can feed it\n * fragmented strings or bytes without constructing a `ReadableStream`.\n */\nexport function createMatrxNdjsonFramer(\n options: MatrxNdjsonFramerOptions = {},\n): MatrxNdjsonFramer {\n const decoder = new TextDecoder();\n let buffer = \"\";\n let lineNumber = 0;\n let finished = false;\n\n const assertOpen = (): void => {\n if (finished) throw new Error(\"Matrx NDJSON framer is already finished\");\n };\n\n const parseLine = (\n line: string,\n atCompletion: boolean,\n ): MatrxStreamEnvelope | null => {\n const currentLineNumber = ++lineNumber;\n const trimmed = line.trim();\n if (!trimmed) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed) as unknown;\n } catch (error) {\n options.onMalformedLine?.({\n line: trimmed,\n error,\n lineNumber: currentLineNumber,\n atCompletion,\n });\n return null;\n }\n\n const envelope = normalizeMatrxStreamEnvelope(parsed);\n if (!envelope) {\n options.onUnknownEnvelope?.(parsed);\n return null;\n }\n options.onValidEnvelope?.({\n raw: parsed,\n envelope,\n line: trimmed,\n lineNumber: currentLineNumber,\n atCompletion,\n });\n return envelope;\n };\n\n const pushDecodedText = (fragment: string): MatrxStreamEnvelope[] => {\n buffer += fragment;\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n const envelopes: MatrxStreamEnvelope[] = [];\n for (const line of lines) {\n const envelope = parseLine(line, false);\n if (envelope) envelopes.push(envelope);\n }\n return envelopes;\n };\n\n return {\n pushText(fragment) {\n assertOpen();\n // Flush any pending byte fragment before switching to explicit text.\n return pushDecodedText(decoder.decode() + fragment);\n },\n pushBytes(fragment) {\n assertOpen();\n return pushDecodedText(decoder.decode(fragment, { stream: true }));\n },\n finish() {\n assertOpen();\n finished = true;\n const envelopes = pushDecodedText(decoder.decode());\n if (buffer.length > 0) {\n const envelope = parseLine(buffer, true);\n buffer = \"\";\n if (envelope) envelopes.push(envelope);\n }\n return envelopes;\n },\n };\n}\n\nfunction readAheadLimit(value: number | undefined): number {\n const limit = value ?? DEFAULT_MATRX_NDJSON_READ_AHEAD;\n if (!Number.isSafeInteger(limit) || limit < 1) {\n throw new RangeError(\"maxReadAhead must be a positive safe integer\");\n }\n return limit;\n}\n\n/**\n * Read and normalize a Matrx NDJSON response body with bounded background\n * read-ahead. Consumer work does not stall the network until `maxReadAhead`\n * complete events are waiting; the bound prevents an abandoned or blocked\n * consumer from growing memory without limit.\n */\nexport async function* readMatrxNdjsonStream(\n body: ReadableStream<Uint8Array>,\n options: ReadMatrxNdjsonOptions = {},\n): AsyncGenerator<MatrxStreamEnvelope, void, undefined> {\n const maxReadAhead = readAheadLimit(options.maxReadAhead);\n const queue: QueueItem[] = [];\n let queuedEventCount = 0;\n let wakeConsumer: (() => void) | null = null;\n let wakeProducer: (() => void) | null = null;\n let readerFinished = false;\n let consumerClosed = false;\n\n const wakeWaitingConsumer = (): void => {\n const wake = wakeConsumer;\n wakeConsumer = null;\n wake?.();\n };\n const wakeWaitingProducer = (): void => {\n const wake = wakeProducer;\n wakeProducer = null;\n wake?.();\n };\n const enqueueTerminal = (item: QueueItem): void => {\n if (consumerClosed) return;\n queue.push(item);\n wakeWaitingConsumer();\n };\n const enqueueEvent = async (value: MatrxStreamEnvelope): Promise<boolean> => {\n while (\n queuedEventCount >= maxReadAhead &&\n !consumerClosed &&\n !options.signal?.aborted\n ) {\n await new Promise<void>((resolve) => {\n wakeProducer = resolve;\n });\n }\n if (consumerClosed || options.signal?.aborted) return false;\n queue.push({ kind: \"event\", value });\n queuedEventCount += 1;\n wakeWaitingConsumer();\n return true;\n };\n const waitForReadCapacity = async (): Promise<boolean> => {\n while (\n queuedEventCount >= maxReadAhead &&\n !consumerClosed &&\n !options.signal?.aborted\n ) {\n await new Promise<void>((resolve) => {\n wakeProducer = resolve;\n });\n }\n return !consumerClosed && !options.signal?.aborted;\n };\n\n const reader = body.getReader();\n const framer = createMatrxNdjsonFramer(options);\n\n const onAbort = (): void => {\n wakeWaitingProducer();\n wakeWaitingConsumer();\n void reader.cancel(options.signal?.reason).catch(() => undefined);\n };\n options.signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (options.signal?.aborted) onAbort();\n\n const readerPromise = (async (): Promise<void> => {\n try {\n while (!options.signal?.aborted && !consumerClosed) {\n if (!(await waitForReadCapacity())) return;\n const { value, done } = await reader.read();\n if (done) break;\n for (const envelope of framer.pushBytes(value)) {\n if (!(await enqueueEvent(envelope))) return;\n }\n }\n\n if (!options.signal?.aborted && !consumerClosed) {\n for (const envelope of framer.finish()) {\n if (!(await enqueueEvent(envelope))) return;\n }\n }\n } catch (error) {\n const aborted =\n options.signal?.aborted ||\n consumerClosed ||\n (error instanceof Error && error.name === \"AbortError\");\n if (!aborted) enqueueTerminal({ kind: \"error\", error });\n } finally {\n readerFinished = true;\n reader.releaseLock();\n enqueueTerminal({ kind: \"done\" });\n }\n })();\n\n try {\n while (true) {\n if (queue.length === 0) {\n if (options.signal?.aborted || readerFinished) return;\n await new Promise<void>((resolve) => {\n wakeConsumer = resolve;\n });\n }\n\n const item = queue.shift();\n if (item?.kind === \"event\") queuedEventCount -= 1;\n wakeWaitingProducer();\n if (!item || item.kind === \"done\") return;\n if (item.kind === \"error\") throw item.error;\n yield item.value;\n }\n } finally {\n consumerClosed = true;\n wakeWaitingProducer();\n wakeWaitingConsumer();\n options.signal?.removeEventListener(\"abort\", onAbort);\n if (!readerFinished) {\n await reader.cancel().catch(() => undefined);\n }\n await readerPromise;\n }\n}\n"]}
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ // stream/sse.ts
4
+ var FRAME_SEPARATOR = /\r\n\r\n|\n\n|\r\r/;
5
+ var LINE_SEPARATOR = /\r\n|\n|\r/;
6
+ function parseMatrxSseFrame(frame) {
7
+ let event = "message";
8
+ let id = null;
9
+ const dataLines = [];
10
+ let sawData = false;
11
+ for (const line of frame.split(LINE_SEPARATOR)) {
12
+ if (line.startsWith(":")) continue;
13
+ if (line.startsWith("event:")) event = line.slice(6).trim();
14
+ else if (line.startsWith("data:")) {
15
+ sawData = true;
16
+ dataLines.push(line.slice(5).replace(/^ /, ""));
17
+ } else if (line.startsWith("id:")) id = line.slice(3).trim();
18
+ }
19
+ const seqCandidate = id !== null && id !== "" ? Number(id) : NaN;
20
+ const seq = Number.isSafeInteger(seqCandidate) && seqCandidate >= 0 ? seqCandidate : null;
21
+ return { event, id, seq, data: sawData ? dataLines.join("\n") : null };
22
+ }
23
+ function createMatrxSseFramer() {
24
+ let buffer = "";
25
+ return {
26
+ push(chunk) {
27
+ buffer += chunk;
28
+ const frames = [];
29
+ for (; ; ) {
30
+ const sep = FRAME_SEPARATOR.exec(buffer);
31
+ if (sep === null) break;
32
+ const frame = buffer.slice(0, sep.index);
33
+ buffer = buffer.slice(sep.index + sep[0].length);
34
+ frames.push(parseMatrxSseFrame(frame));
35
+ }
36
+ return frames;
37
+ },
38
+ flush() {
39
+ const rest = buffer;
40
+ buffer = "";
41
+ return { incomplete: rest.length > 0 ? rest : null };
42
+ }
43
+ };
44
+ }
45
+ async function* readMatrxSseStream(stream, options = {}) {
46
+ const reader = stream.getReader();
47
+ const decoder = new TextDecoder();
48
+ const framer = createMatrxSseFramer();
49
+ try {
50
+ for (; ; ) {
51
+ const { value, done } = await reader.read();
52
+ if (done) break;
53
+ const frames = framer.push(decoder.decode(value, { stream: true }));
54
+ for (const frame of frames) yield frame;
55
+ }
56
+ const tail = framer.push(decoder.decode());
57
+ for (const frame of tail) yield frame;
58
+ const { incomplete } = framer.flush();
59
+ if (incomplete !== null) options.onIncomplete?.(incomplete);
60
+ } finally {
61
+ reader.releaseLock();
62
+ }
63
+ }
64
+
65
+ exports.createMatrxSseFramer = createMatrxSseFramer;
66
+ exports.parseMatrxSseFrame = parseMatrxSseFrame;
67
+ exports.readMatrxSseStream = readMatrxSseStream;
68
+ //# sourceMappingURL=sse.cjs.map
69
+ //# sourceMappingURL=sse.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../stream/sse.ts"],"names":[],"mappings":"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","file":"sse.cjs","sourcesContent":["/**\n * `@ai-matrx/agents/stream/sse` — the Matrx SSE frame kernel.\n *\n * Two Matrx clients hand-rolled the identical `text/event-stream` framing —\n * the same separator regex, the same field parsing, the same CRLF incident —\n * for the durable rejoin endpoints (`/runtime/.../events/stream`,\n * `/runs/.../events/stream`). This module is that framing extracted ONCE, as a\n * pure incremental parser, so a host keeps only what is genuinely host policy:\n * the fetch, the stall timer, the retry budget, and what each event MEANS.\n *\n * Contract, mirroring `stream/ndjson`:\n * - Pure and effect-free: no fetch, no timers, no globals. Importing this\n * module performs no work.\n * - Incremental across arbitrary chunk boundaries: a frame split anywhere —\n * mid-line, mid-separator, mid-UTF-8 when using the byte reader — parses\n * identically to one delivered whole.\n * - All three SSE line terminators (`\\r\\n`, `\\n`, `\\r`) and all three frame\n * separators are handled — the CRLF-vs-LF divergence that bit production is\n * covered by construction and by test.\n * - Comment-only frames (heartbeats, `:` lines) ARE emitted (with\n * `data === null`) because hosts use any parsed frame as liveness proof to\n * reset stall timers and retry budgets. Frames with data join multi-line\n * `data:` fields with `\\n` per the SSE spec.\n * - `id:` is surfaced raw AND, when it is a safe integer, as `seq` — the\n * `Last-Event-ID` cursor both Matrx rejoin endpoints use for durable replay.\n * Cursor ADVANCEMENT (`seq > cursor`) stays host-side, next to the retry.\n */\n\nexport interface MatrxSseFrame {\n /** `event:` field; the SSE default \"message\" when absent. */\n event: string;\n /** `id:` field, raw, when present. */\n id: string | null;\n /**\n * `id:` parsed as a non-negative safe integer, else null. Matrx rejoin\n * streams use integer ids as the `Last-Event-ID` replay cursor.\n */\n seq: number | null;\n /**\n * Joined `data:` lines (`\\n`-separated per spec), or null for a frame with\n * no data field at all (e.g. a comment-only heartbeat). An empty-string\n * data field is `\"\"`, not null.\n */\n data: string | null;\n}\n\nconst FRAME_SEPARATOR = /\\r\\n\\r\\n|\\n\\n|\\r\\r/;\nconst LINE_SEPARATOR = /\\r\\n|\\n|\\r/;\n\n/** Parse ONE complete frame's text (no trailing separator). */\nexport function parseMatrxSseFrame(frame: string): MatrxSseFrame {\n let event = \"message\";\n let id: string | null = null;\n const dataLines: string[] = [];\n let sawData = false;\n\n for (const line of frame.split(LINE_SEPARATOR)) {\n if (line.startsWith(\":\")) continue;\n if (line.startsWith(\"event:\")) event = line.slice(6).trim();\n else if (line.startsWith(\"data:\")) {\n sawData = true;\n dataLines.push(line.slice(5).replace(/^ /, \"\"));\n } else if (line.startsWith(\"id:\")) id = line.slice(3).trim();\n }\n\n const seqCandidate = id !== null && id !== \"\" ? Number(id) : NaN;\n const seq =\n Number.isSafeInteger(seqCandidate) && seqCandidate >= 0\n ? seqCandidate\n : null;\n\n return { event, id, seq, data: sawData ? dataLines.join(\"\\n\") : null };\n}\n\nexport interface MatrxSseFramer {\n /** Feed a decoded text chunk; returns every frame it completed. */\n push(chunk: string): MatrxSseFrame[];\n /**\n * Signal end of input. A non-empty trailing buffer is an UNTERMINATED frame:\n * per the SSE spec it was never dispatched, so it is returned separately for\n * the host to treat as diagnostic, never as a delivered event.\n */\n flush(): { incomplete: string | null };\n}\n\n/** Incremental SSE framer over already-decoded text. */\nexport function createMatrxSseFramer(): MatrxSseFramer {\n let buffer = \"\";\n return {\n push(chunk: string): MatrxSseFrame[] {\n buffer += chunk;\n const frames: MatrxSseFrame[] = [];\n for (;;) {\n const sep = FRAME_SEPARATOR.exec(buffer);\n if (sep === null) break;\n // A lone trailing `\\r` could be the first half of `\\r\\n\\r\\n`'s final\n // newline — but the separator regex only matched what is already\n // complete, so the slice below is always safe.\n const frame = buffer.slice(0, sep.index);\n buffer = buffer.slice(sep.index + sep[0].length);\n frames.push(parseMatrxSseFrame(frame));\n }\n return frames;\n },\n flush() {\n const rest = buffer;\n buffer = \"\";\n return { incomplete: rest.length > 0 ? rest : null };\n },\n };\n}\n\nexport interface ReadMatrxSseOptions {\n /**\n * Called with any unterminated trailing text at stream end (a frame the\n * server never finished — diagnostic, not a delivered event).\n */\n onIncomplete?: (text: string) => void;\n}\n\n/**\n * Async-iterate the frames of a byte stream (e.g. `response.body`), handling\n * split UTF-8 across chunk boundaries. Cancellation follows the reader: abort\n * the fetch and the iterator ends; a transport error after complete frames\n * were yielded surfaces AFTER those frames, with its original cause.\n */\nexport async function* readMatrxSseStream(\n stream: ReadableStream<Uint8Array>,\n options: ReadMatrxSseOptions = {},\n): AsyncGenerator<MatrxSseFrame, void, undefined> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n const framer = createMatrxSseFramer();\n try {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n const frames = framer.push(decoder.decode(value, { stream: true }));\n for (const frame of frames) yield frame;\n }\n const tail = framer.push(decoder.decode());\n for (const frame of tail) yield frame;\n const { incomplete } = framer.flush();\n if (incomplete !== null) options.onIncomplete?.(incomplete);\n } finally {\n reader.releaseLock();\n }\n}\n"]}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@ai-matrx/agents/stream/sse` — the Matrx SSE frame kernel.
3
+ *
4
+ * Two Matrx clients hand-rolled the identical `text/event-stream` framing —
5
+ * the same separator regex, the same field parsing, the same CRLF incident —
6
+ * for the durable rejoin endpoints (`/runtime/.../events/stream`,
7
+ * `/runs/.../events/stream`). This module is that framing extracted ONCE, as a
8
+ * pure incremental parser, so a host keeps only what is genuinely host policy:
9
+ * the fetch, the stall timer, the retry budget, and what each event MEANS.
10
+ *
11
+ * Contract, mirroring `stream/ndjson`:
12
+ * - Pure and effect-free: no fetch, no timers, no globals. Importing this
13
+ * module performs no work.
14
+ * - Incremental across arbitrary chunk boundaries: a frame split anywhere —
15
+ * mid-line, mid-separator, mid-UTF-8 when using the byte reader — parses
16
+ * identically to one delivered whole.
17
+ * - All three SSE line terminators (`\r\n`, `\n`, `\r`) and all three frame
18
+ * separators are handled — the CRLF-vs-LF divergence that bit production is
19
+ * covered by construction and by test.
20
+ * - Comment-only frames (heartbeats, `:` lines) ARE emitted (with
21
+ * `data === null`) because hosts use any parsed frame as liveness proof to
22
+ * reset stall timers and retry budgets. Frames with data join multi-line
23
+ * `data:` fields with `\n` per the SSE spec.
24
+ * - `id:` is surfaced raw AND, when it is a safe integer, as `seq` — the
25
+ * `Last-Event-ID` cursor both Matrx rejoin endpoints use for durable replay.
26
+ * Cursor ADVANCEMENT (`seq > cursor`) stays host-side, next to the retry.
27
+ */
28
+ interface MatrxSseFrame {
29
+ /** `event:` field; the SSE default "message" when absent. */
30
+ event: string;
31
+ /** `id:` field, raw, when present. */
32
+ id: string | null;
33
+ /**
34
+ * `id:` parsed as a non-negative safe integer, else null. Matrx rejoin
35
+ * streams use integer ids as the `Last-Event-ID` replay cursor.
36
+ */
37
+ seq: number | null;
38
+ /**
39
+ * Joined `data:` lines (`\n`-separated per spec), or null for a frame with
40
+ * no data field at all (e.g. a comment-only heartbeat). An empty-string
41
+ * data field is `""`, not null.
42
+ */
43
+ data: string | null;
44
+ }
45
+ /** Parse ONE complete frame's text (no trailing separator). */
46
+ declare function parseMatrxSseFrame(frame: string): MatrxSseFrame;
47
+ interface MatrxSseFramer {
48
+ /** Feed a decoded text chunk; returns every frame it completed. */
49
+ push(chunk: string): MatrxSseFrame[];
50
+ /**
51
+ * Signal end of input. A non-empty trailing buffer is an UNTERMINATED frame:
52
+ * per the SSE spec it was never dispatched, so it is returned separately for
53
+ * the host to treat as diagnostic, never as a delivered event.
54
+ */
55
+ flush(): {
56
+ incomplete: string | null;
57
+ };
58
+ }
59
+ /** Incremental SSE framer over already-decoded text. */
60
+ declare function createMatrxSseFramer(): MatrxSseFramer;
61
+ interface ReadMatrxSseOptions {
62
+ /**
63
+ * Called with any unterminated trailing text at stream end (a frame the
64
+ * server never finished — diagnostic, not a delivered event).
65
+ */
66
+ onIncomplete?: (text: string) => void;
67
+ }
68
+ /**
69
+ * Async-iterate the frames of a byte stream (e.g. `response.body`), handling
70
+ * split UTF-8 across chunk boundaries. Cancellation follows the reader: abort
71
+ * the fetch and the iterator ends; a transport error after complete frames
72
+ * were yielded surfaces AFTER those frames, with its original cause.
73
+ */
74
+ declare function readMatrxSseStream(stream: ReadableStream<Uint8Array>, options?: ReadMatrxSseOptions): AsyncGenerator<MatrxSseFrame, void, undefined>;
75
+
76
+ export { type MatrxSseFrame, type MatrxSseFramer, type ReadMatrxSseOptions, createMatrxSseFramer, parseMatrxSseFrame, readMatrxSseStream };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@ai-matrx/agents/stream/sse` — the Matrx SSE frame kernel.
3
+ *
4
+ * Two Matrx clients hand-rolled the identical `text/event-stream` framing —
5
+ * the same separator regex, the same field parsing, the same CRLF incident —
6
+ * for the durable rejoin endpoints (`/runtime/.../events/stream`,
7
+ * `/runs/.../events/stream`). This module is that framing extracted ONCE, as a
8
+ * pure incremental parser, so a host keeps only what is genuinely host policy:
9
+ * the fetch, the stall timer, the retry budget, and what each event MEANS.
10
+ *
11
+ * Contract, mirroring `stream/ndjson`:
12
+ * - Pure and effect-free: no fetch, no timers, no globals. Importing this
13
+ * module performs no work.
14
+ * - Incremental across arbitrary chunk boundaries: a frame split anywhere —
15
+ * mid-line, mid-separator, mid-UTF-8 when using the byte reader — parses
16
+ * identically to one delivered whole.
17
+ * - All three SSE line terminators (`\r\n`, `\n`, `\r`) and all three frame
18
+ * separators are handled — the CRLF-vs-LF divergence that bit production is
19
+ * covered by construction and by test.
20
+ * - Comment-only frames (heartbeats, `:` lines) ARE emitted (with
21
+ * `data === null`) because hosts use any parsed frame as liveness proof to
22
+ * reset stall timers and retry budgets. Frames with data join multi-line
23
+ * `data:` fields with `\n` per the SSE spec.
24
+ * - `id:` is surfaced raw AND, when it is a safe integer, as `seq` — the
25
+ * `Last-Event-ID` cursor both Matrx rejoin endpoints use for durable replay.
26
+ * Cursor ADVANCEMENT (`seq > cursor`) stays host-side, next to the retry.
27
+ */
28
+ interface MatrxSseFrame {
29
+ /** `event:` field; the SSE default "message" when absent. */
30
+ event: string;
31
+ /** `id:` field, raw, when present. */
32
+ id: string | null;
33
+ /**
34
+ * `id:` parsed as a non-negative safe integer, else null. Matrx rejoin
35
+ * streams use integer ids as the `Last-Event-ID` replay cursor.
36
+ */
37
+ seq: number | null;
38
+ /**
39
+ * Joined `data:` lines (`\n`-separated per spec), or null for a frame with
40
+ * no data field at all (e.g. a comment-only heartbeat). An empty-string
41
+ * data field is `""`, not null.
42
+ */
43
+ data: string | null;
44
+ }
45
+ /** Parse ONE complete frame's text (no trailing separator). */
46
+ declare function parseMatrxSseFrame(frame: string): MatrxSseFrame;
47
+ interface MatrxSseFramer {
48
+ /** Feed a decoded text chunk; returns every frame it completed. */
49
+ push(chunk: string): MatrxSseFrame[];
50
+ /**
51
+ * Signal end of input. A non-empty trailing buffer is an UNTERMINATED frame:
52
+ * per the SSE spec it was never dispatched, so it is returned separately for
53
+ * the host to treat as diagnostic, never as a delivered event.
54
+ */
55
+ flush(): {
56
+ incomplete: string | null;
57
+ };
58
+ }
59
+ /** Incremental SSE framer over already-decoded text. */
60
+ declare function createMatrxSseFramer(): MatrxSseFramer;
61
+ interface ReadMatrxSseOptions {
62
+ /**
63
+ * Called with any unterminated trailing text at stream end (a frame the
64
+ * server never finished — diagnostic, not a delivered event).
65
+ */
66
+ onIncomplete?: (text: string) => void;
67
+ }
68
+ /**
69
+ * Async-iterate the frames of a byte stream (e.g. `response.body`), handling
70
+ * split UTF-8 across chunk boundaries. Cancellation follows the reader: abort
71
+ * the fetch and the iterator ends; a transport error after complete frames
72
+ * were yielded surfaces AFTER those frames, with its original cause.
73
+ */
74
+ declare function readMatrxSseStream(stream: ReadableStream<Uint8Array>, options?: ReadMatrxSseOptions): AsyncGenerator<MatrxSseFrame, void, undefined>;
75
+
76
+ export { type MatrxSseFrame, type MatrxSseFramer, type ReadMatrxSseOptions, createMatrxSseFramer, parseMatrxSseFrame, readMatrxSseStream };
@@ -0,0 +1,65 @@
1
+ // stream/sse.ts
2
+ var FRAME_SEPARATOR = /\r\n\r\n|\n\n|\r\r/;
3
+ var LINE_SEPARATOR = /\r\n|\n|\r/;
4
+ function parseMatrxSseFrame(frame) {
5
+ let event = "message";
6
+ let id = null;
7
+ const dataLines = [];
8
+ let sawData = false;
9
+ for (const line of frame.split(LINE_SEPARATOR)) {
10
+ if (line.startsWith(":")) continue;
11
+ if (line.startsWith("event:")) event = line.slice(6).trim();
12
+ else if (line.startsWith("data:")) {
13
+ sawData = true;
14
+ dataLines.push(line.slice(5).replace(/^ /, ""));
15
+ } else if (line.startsWith("id:")) id = line.slice(3).trim();
16
+ }
17
+ const seqCandidate = id !== null && id !== "" ? Number(id) : NaN;
18
+ const seq = Number.isSafeInteger(seqCandidate) && seqCandidate >= 0 ? seqCandidate : null;
19
+ return { event, id, seq, data: sawData ? dataLines.join("\n") : null };
20
+ }
21
+ function createMatrxSseFramer() {
22
+ let buffer = "";
23
+ return {
24
+ push(chunk) {
25
+ buffer += chunk;
26
+ const frames = [];
27
+ for (; ; ) {
28
+ const sep = FRAME_SEPARATOR.exec(buffer);
29
+ if (sep === null) break;
30
+ const frame = buffer.slice(0, sep.index);
31
+ buffer = buffer.slice(sep.index + sep[0].length);
32
+ frames.push(parseMatrxSseFrame(frame));
33
+ }
34
+ return frames;
35
+ },
36
+ flush() {
37
+ const rest = buffer;
38
+ buffer = "";
39
+ return { incomplete: rest.length > 0 ? rest : null };
40
+ }
41
+ };
42
+ }
43
+ async function* readMatrxSseStream(stream, options = {}) {
44
+ const reader = stream.getReader();
45
+ const decoder = new TextDecoder();
46
+ const framer = createMatrxSseFramer();
47
+ try {
48
+ for (; ; ) {
49
+ const { value, done } = await reader.read();
50
+ if (done) break;
51
+ const frames = framer.push(decoder.decode(value, { stream: true }));
52
+ for (const frame of frames) yield frame;
53
+ }
54
+ const tail = framer.push(decoder.decode());
55
+ for (const frame of tail) yield frame;
56
+ const { incomplete } = framer.flush();
57
+ if (incomplete !== null) options.onIncomplete?.(incomplete);
58
+ } finally {
59
+ reader.releaseLock();
60
+ }
61
+ }
62
+
63
+ export { createMatrxSseFramer, parseMatrxSseFrame, readMatrxSseStream };
64
+ //# sourceMappingURL=sse.js.map
65
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../stream/sse.ts"],"names":[],"mappings":"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","file":"sse.js","sourcesContent":["/**\n * `@ai-matrx/agents/stream/sse` — the Matrx SSE frame kernel.\n *\n * Two Matrx clients hand-rolled the identical `text/event-stream` framing —\n * the same separator regex, the same field parsing, the same CRLF incident —\n * for the durable rejoin endpoints (`/runtime/.../events/stream`,\n * `/runs/.../events/stream`). This module is that framing extracted ONCE, as a\n * pure incremental parser, so a host keeps only what is genuinely host policy:\n * the fetch, the stall timer, the retry budget, and what each event MEANS.\n *\n * Contract, mirroring `stream/ndjson`:\n * - Pure and effect-free: no fetch, no timers, no globals. Importing this\n * module performs no work.\n * - Incremental across arbitrary chunk boundaries: a frame split anywhere —\n * mid-line, mid-separator, mid-UTF-8 when using the byte reader — parses\n * identically to one delivered whole.\n * - All three SSE line terminators (`\\r\\n`, `\\n`, `\\r`) and all three frame\n * separators are handled — the CRLF-vs-LF divergence that bit production is\n * covered by construction and by test.\n * - Comment-only frames (heartbeats, `:` lines) ARE emitted (with\n * `data === null`) because hosts use any parsed frame as liveness proof to\n * reset stall timers and retry budgets. Frames with data join multi-line\n * `data:` fields with `\\n` per the SSE spec.\n * - `id:` is surfaced raw AND, when it is a safe integer, as `seq` — the\n * `Last-Event-ID` cursor both Matrx rejoin endpoints use for durable replay.\n * Cursor ADVANCEMENT (`seq > cursor`) stays host-side, next to the retry.\n */\n\nexport interface MatrxSseFrame {\n /** `event:` field; the SSE default \"message\" when absent. */\n event: string;\n /** `id:` field, raw, when present. */\n id: string | null;\n /**\n * `id:` parsed as a non-negative safe integer, else null. Matrx rejoin\n * streams use integer ids as the `Last-Event-ID` replay cursor.\n */\n seq: number | null;\n /**\n * Joined `data:` lines (`\\n`-separated per spec), or null for a frame with\n * no data field at all (e.g. a comment-only heartbeat). An empty-string\n * data field is `\"\"`, not null.\n */\n data: string | null;\n}\n\nconst FRAME_SEPARATOR = /\\r\\n\\r\\n|\\n\\n|\\r\\r/;\nconst LINE_SEPARATOR = /\\r\\n|\\n|\\r/;\n\n/** Parse ONE complete frame's text (no trailing separator). */\nexport function parseMatrxSseFrame(frame: string): MatrxSseFrame {\n let event = \"message\";\n let id: string | null = null;\n const dataLines: string[] = [];\n let sawData = false;\n\n for (const line of frame.split(LINE_SEPARATOR)) {\n if (line.startsWith(\":\")) continue;\n if (line.startsWith(\"event:\")) event = line.slice(6).trim();\n else if (line.startsWith(\"data:\")) {\n sawData = true;\n dataLines.push(line.slice(5).replace(/^ /, \"\"));\n } else if (line.startsWith(\"id:\")) id = line.slice(3).trim();\n }\n\n const seqCandidate = id !== null && id !== \"\" ? Number(id) : NaN;\n const seq =\n Number.isSafeInteger(seqCandidate) && seqCandidate >= 0\n ? seqCandidate\n : null;\n\n return { event, id, seq, data: sawData ? dataLines.join(\"\\n\") : null };\n}\n\nexport interface MatrxSseFramer {\n /** Feed a decoded text chunk; returns every frame it completed. */\n push(chunk: string): MatrxSseFrame[];\n /**\n * Signal end of input. A non-empty trailing buffer is an UNTERMINATED frame:\n * per the SSE spec it was never dispatched, so it is returned separately for\n * the host to treat as diagnostic, never as a delivered event.\n */\n flush(): { incomplete: string | null };\n}\n\n/** Incremental SSE framer over already-decoded text. */\nexport function createMatrxSseFramer(): MatrxSseFramer {\n let buffer = \"\";\n return {\n push(chunk: string): MatrxSseFrame[] {\n buffer += chunk;\n const frames: MatrxSseFrame[] = [];\n for (;;) {\n const sep = FRAME_SEPARATOR.exec(buffer);\n if (sep === null) break;\n // A lone trailing `\\r` could be the first half of `\\r\\n\\r\\n`'s final\n // newline — but the separator regex only matched what is already\n // complete, so the slice below is always safe.\n const frame = buffer.slice(0, sep.index);\n buffer = buffer.slice(sep.index + sep[0].length);\n frames.push(parseMatrxSseFrame(frame));\n }\n return frames;\n },\n flush() {\n const rest = buffer;\n buffer = \"\";\n return { incomplete: rest.length > 0 ? rest : null };\n },\n };\n}\n\nexport interface ReadMatrxSseOptions {\n /**\n * Called with any unterminated trailing text at stream end (a frame the\n * server never finished — diagnostic, not a delivered event).\n */\n onIncomplete?: (text: string) => void;\n}\n\n/**\n * Async-iterate the frames of a byte stream (e.g. `response.body`), handling\n * split UTF-8 across chunk boundaries. Cancellation follows the reader: abort\n * the fetch and the iterator ends; a transport error after complete frames\n * were yielded surfaces AFTER those frames, with its original cause.\n */\nexport async function* readMatrxSseStream(\n stream: ReadableStream<Uint8Array>,\n options: ReadMatrxSseOptions = {},\n): AsyncGenerator<MatrxSseFrame, void, undefined> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n const framer = createMatrxSseFramer();\n try {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n const frames = framer.push(decoder.decode(value, { stream: true }));\n for (const frame of frames) yield frame;\n }\n const tail = framer.push(decoder.decode());\n for (const frame of tail) yield frame;\n const { incomplete } = framer.flush();\n if (incomplete !== null) options.onIncomplete?.(incomplete);\n } finally {\n reader.releaseLock();\n }\n}\n"]}