@ai-matrx/agents 0.2.1 → 0.3.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.
- package/CHANGELOG.md +19 -3
- package/README.md +30 -6
- package/dist/index.cjs +289 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +287 -50
- package/dist/index.js.map +1 -1
- package/dist/projection/request.cjs +1 -1
- package/dist/projection/request.cjs.map +1 -1
- package/dist/projection/request.js +1 -1
- package/dist/projection/request.js.map +1 -1
- package/dist/projection/workflow.cjs +145 -18
- package/dist/projection/workflow.cjs.map +1 -1
- package/dist/projection/workflow.d.cts +18 -1
- package/dist/projection/workflow.d.ts +18 -1
- package/dist/projection/workflow.js +145 -19
- package/dist/projection/workflow.js.map +1 -1
- package/dist/stream/ndjson.cjs +143 -30
- package/dist/stream/ndjson.cjs.map +1 -1
- package/dist/stream/ndjson.d.cts +42 -7
- package/dist/stream/ndjson.d.ts +42 -7
- package/dist/stream/ndjson.js +142 -31
- package/dist/stream/ndjson.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-08-24
|
|
4
|
+
|
|
5
|
+
- Added a transport-independent incremental NDJSON framer for fragmented text
|
|
6
|
+
and bytes, including split UTF-8 and unterminated trailing input.
|
|
7
|
+
- Added exact valid-envelope observation, physical-line malformed diagnostics,
|
|
8
|
+
cancellation proof, bounded configurable read-ahead, and ordered partial
|
|
9
|
+
transport failure behavior.
|
|
10
|
+
- Preserved top-level `stream_seq` through wire normalization so projector
|
|
11
|
+
replay suppression receives the server's transport sequence.
|
|
12
|
+
- Aligned tool status semantics with execution: server `tool_started` remains
|
|
13
|
+
streaming; only `tool_delegated` suspends for client work.
|
|
14
|
+
- Bounded workflow render-block assembly by open sets, frames, and UTF-8 bytes;
|
|
15
|
+
malformed or invalid completions now produce typed diagnostics instead of
|
|
16
|
+
disappearing silently.
|
|
17
|
+
- Expanded TypeScript checking to include every projection source and test.
|
|
18
|
+
|
|
3
19
|
## 0.2.1 — 2026-08-24
|
|
4
20
|
|
|
5
21
|
- Published the documented workflow projection entry point that workspace
|
|
6
22
|
consumers already used but the immutable 0.2.0 registry artifact omitted.
|
|
23
|
+
- Added the portable workflow `node_stream` projection inlet with strict
|
|
24
|
+
answer/reasoning separation, bounded render-block assembly, replay
|
|
25
|
+
suppression, and shadowed-text state.
|
|
7
26
|
- Added every request and workflow projection test to the normal package test
|
|
8
27
|
gate; the previous Vitest include list silently ran only stream and
|
|
9
28
|
presentation tests.
|
|
10
29
|
|
|
11
30
|
## 0.2.0 — 2026-08-22
|
|
12
31
|
|
|
13
|
-
- Added the portable workflow `node_stream` projection inlet with strict
|
|
14
|
-
answer/reasoning separation, bounded render-block assembly, replay
|
|
15
|
-
suppression, and shadowed-text state.
|
|
16
32
|
- Added a framework-free request event projector for identity, status, answer/reasoning, phases, operations, tools, render blocks, Content IR metadata, completion, and errors.
|
|
17
33
|
- Added golden lifecycle fixtures, replay suppression, and copy-on-write guarantees.
|
|
18
34
|
- Added conditional ESM/CommonJS artifacts and loader canaries for mixed Vite, Next.js, and Jest consumers.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @ai-matrx/agents
|
|
2
2
|
|
|
3
|
-
Portable client-side primitives for AI Matrx agent applications.
|
|
4
|
-
|
|
5
|
-
Creator-facing result
|
|
3
|
+
Portable client-side primitives for AI Matrx agent applications. The package
|
|
4
|
+
standardizes the stream wire, pure request/workflow projection, and safe
|
|
5
|
+
Creator-facing result boundaries without importing React, Redux, Next.js, or
|
|
6
6
|
application code.
|
|
7
7
|
|
|
8
8
|
## Install
|
|
@@ -25,8 +25,30 @@ for await (const envelope of readMatrxNdjsonStream(response.body!, {
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
The reader preserves split UTF-8, drains the network independently of consumer
|
|
28
|
-
work
|
|
29
|
-
reports malformed or unknown
|
|
28
|
+
work up to the configurable `maxReadAhead` bound, supports cancellation,
|
|
29
|
+
normalizes full and compact Matrx envelopes, and reports malformed or unknown
|
|
30
|
+
input through explicit callbacks. `onValidEnvelope` observes the exact parsed
|
|
31
|
+
wire value before normalization without consuming it. Full-envelope
|
|
32
|
+
`stream_seq` is preserved for projector replay suppression.
|
|
33
|
+
|
|
34
|
+
## Frame non-ReadableStream transports
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createMatrxNdjsonFramer } from "@ai-matrx/agents/stream/ndjson";
|
|
38
|
+
|
|
39
|
+
const framer = createMatrxNdjsonFramer({
|
|
40
|
+
onMalformedLine: reportProtocolDamage,
|
|
41
|
+
onValidEnvelope: persistRawEnvelope,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
handleEvents(framer.pushBytes(extensionMessageBytes));
|
|
45
|
+
handleEvents(framer.pushText(desktopBridgeFragment));
|
|
46
|
+
handleEvents(framer.finish());
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The same incremental framer works with browser-extension messages, desktop
|
|
50
|
+
bridges, WebSockets, and tests. `finish()` flushes split UTF-8 and diagnoses an
|
|
51
|
+
invalid unterminated final line with `atCompletion: true`.
|
|
30
52
|
|
|
31
53
|
## Present a settled result safely
|
|
32
54
|
|
|
@@ -55,7 +77,9 @@ node = projectWorkflowNodeEvent(node, nodeStreamFrame);
|
|
|
55
77
|
This is the single workflow presentation inlet. It keeps answer and private
|
|
56
78
|
reasoning separate, rejects replayed frames, assembles bounded server
|
|
57
79
|
`render_block` snapshots, and prevents shadowed text from being interpreted as
|
|
58
|
-
a second copy of the same content.
|
|
80
|
+
a second copy of the same content. Configure `maxOpenFrameSets`,
|
|
81
|
+
`maxFramesPerBlock`, and `maxBytesPerBlock` when creating the projection;
|
|
82
|
+
rejected or malformed blocks are observable through `lastRenderBlockIssue`.
|
|
59
83
|
|
|
60
84
|
## Runtime support
|
|
61
85
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// stream/ndjson.ts
|
|
4
|
+
var DEFAULT_MATRX_NDJSON_READ_AHEAD = 64;
|
|
4
5
|
function isRecord(value) {
|
|
5
6
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
7
|
}
|
|
7
8
|
function normalizeMatrxStreamEnvelope(value) {
|
|
8
9
|
if (!isRecord(value)) return null;
|
|
9
10
|
if (typeof value.event === "string") {
|
|
10
|
-
|
|
11
|
+
const streamSeq = typeof value.stream_seq === "number" && Number.isFinite(value.stream_seq) ? value.stream_seq : void 0;
|
|
12
|
+
return {
|
|
13
|
+
event: value.event,
|
|
14
|
+
data: value.data,
|
|
15
|
+
...streamSeq === void 0 ? {} : { stream_seq: streamSeq }
|
|
16
|
+
};
|
|
11
17
|
}
|
|
12
18
|
if (value.e === "c" && typeof value.t === "string") {
|
|
13
19
|
return { event: "chunk", data: { text: value.t } };
|
|
@@ -17,74 +23,179 @@ function normalizeMatrxStreamEnvelope(value) {
|
|
|
17
23
|
}
|
|
18
24
|
return null;
|
|
19
25
|
}
|
|
20
|
-
|
|
21
|
-
const queue = [];
|
|
22
|
-
let wakeConsumer = null;
|
|
23
|
-
let readerFinished = false;
|
|
24
|
-
const enqueue = (item) => {
|
|
25
|
-
queue.push(item);
|
|
26
|
-
const wake = wakeConsumer;
|
|
27
|
-
wakeConsumer = null;
|
|
28
|
-
wake?.();
|
|
29
|
-
};
|
|
30
|
-
const reader = body.getReader();
|
|
26
|
+
function createMatrxNdjsonFramer(options = {}) {
|
|
31
27
|
const decoder = new TextDecoder();
|
|
32
|
-
|
|
28
|
+
let buffer = "";
|
|
29
|
+
let lineNumber = 0;
|
|
30
|
+
let finished = false;
|
|
31
|
+
const assertOpen = () => {
|
|
32
|
+
if (finished) throw new Error("Matrx NDJSON framer is already finished");
|
|
33
|
+
};
|
|
34
|
+
const parseLine = (line, atCompletion) => {
|
|
35
|
+
const currentLineNumber = ++lineNumber;
|
|
33
36
|
const trimmed = line.trim();
|
|
34
|
-
if (!trimmed) return;
|
|
37
|
+
if (!trimmed) return null;
|
|
35
38
|
let parsed;
|
|
36
39
|
try {
|
|
37
40
|
parsed = JSON.parse(trimmed);
|
|
38
41
|
} catch (error) {
|
|
39
|
-
options.onMalformedLine?.({
|
|
40
|
-
|
|
42
|
+
options.onMalformedLine?.({
|
|
43
|
+
line: trimmed,
|
|
44
|
+
error,
|
|
45
|
+
lineNumber: currentLineNumber,
|
|
46
|
+
atCompletion
|
|
47
|
+
});
|
|
48
|
+
return null;
|
|
41
49
|
}
|
|
42
50
|
const envelope = normalizeMatrxStreamEnvelope(parsed);
|
|
43
|
-
if (envelope) {
|
|
44
|
-
enqueue({ kind: "event", value: envelope });
|
|
45
|
-
} else {
|
|
51
|
+
if (!envelope) {
|
|
46
52
|
options.onUnknownEnvelope?.(parsed);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
options.onValidEnvelope?.({
|
|
56
|
+
raw: parsed,
|
|
57
|
+
envelope,
|
|
58
|
+
line: trimmed,
|
|
59
|
+
lineNumber: currentLineNumber,
|
|
60
|
+
atCompletion
|
|
61
|
+
});
|
|
62
|
+
return envelope;
|
|
63
|
+
};
|
|
64
|
+
const pushDecodedText = (fragment) => {
|
|
65
|
+
buffer += fragment;
|
|
66
|
+
const lines = buffer.split("\n");
|
|
67
|
+
buffer = lines.pop() ?? "";
|
|
68
|
+
const envelopes = [];
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const envelope = parseLine(line, false);
|
|
71
|
+
if (envelope) envelopes.push(envelope);
|
|
47
72
|
}
|
|
73
|
+
return envelopes;
|
|
48
74
|
};
|
|
75
|
+
return {
|
|
76
|
+
pushText(fragment) {
|
|
77
|
+
assertOpen();
|
|
78
|
+
return pushDecodedText(decoder.decode() + fragment);
|
|
79
|
+
},
|
|
80
|
+
pushBytes(fragment) {
|
|
81
|
+
assertOpen();
|
|
82
|
+
return pushDecodedText(decoder.decode(fragment, { stream: true }));
|
|
83
|
+
},
|
|
84
|
+
finish() {
|
|
85
|
+
assertOpen();
|
|
86
|
+
finished = true;
|
|
87
|
+
const envelopes = pushDecodedText(decoder.decode());
|
|
88
|
+
if (buffer.length > 0) {
|
|
89
|
+
const envelope = parseLine(buffer, true);
|
|
90
|
+
buffer = "";
|
|
91
|
+
if (envelope) envelopes.push(envelope);
|
|
92
|
+
}
|
|
93
|
+
return envelopes;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function readAheadLimit(value) {
|
|
98
|
+
const limit = value ?? DEFAULT_MATRX_NDJSON_READ_AHEAD;
|
|
99
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
100
|
+
throw new RangeError("maxReadAhead must be a positive safe integer");
|
|
101
|
+
}
|
|
102
|
+
return limit;
|
|
103
|
+
}
|
|
104
|
+
async function* readMatrxNdjsonStream(body, options = {}) {
|
|
105
|
+
const maxReadAhead = readAheadLimit(options.maxReadAhead);
|
|
106
|
+
const queue = [];
|
|
107
|
+
let queuedEventCount = 0;
|
|
108
|
+
let wakeConsumer = null;
|
|
109
|
+
let wakeProducer = null;
|
|
110
|
+
let readerFinished = false;
|
|
111
|
+
let consumerClosed = false;
|
|
112
|
+
const wakeWaitingConsumer = () => {
|
|
113
|
+
const wake = wakeConsumer;
|
|
114
|
+
wakeConsumer = null;
|
|
115
|
+
wake?.();
|
|
116
|
+
};
|
|
117
|
+
const wakeWaitingProducer = () => {
|
|
118
|
+
const wake = wakeProducer;
|
|
119
|
+
wakeProducer = null;
|
|
120
|
+
wake?.();
|
|
121
|
+
};
|
|
122
|
+
const enqueueTerminal = (item) => {
|
|
123
|
+
if (consumerClosed) return;
|
|
124
|
+
queue.push(item);
|
|
125
|
+
wakeWaitingConsumer();
|
|
126
|
+
};
|
|
127
|
+
const enqueueEvent = async (value) => {
|
|
128
|
+
while (queuedEventCount >= maxReadAhead && !consumerClosed && !options.signal?.aborted) {
|
|
129
|
+
await new Promise((resolve) => {
|
|
130
|
+
wakeProducer = resolve;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (consumerClosed || options.signal?.aborted) return false;
|
|
134
|
+
queue.push({ kind: "event", value });
|
|
135
|
+
queuedEventCount += 1;
|
|
136
|
+
wakeWaitingConsumer();
|
|
137
|
+
return true;
|
|
138
|
+
};
|
|
139
|
+
const waitForReadCapacity = async () => {
|
|
140
|
+
while (queuedEventCount >= maxReadAhead && !consumerClosed && !options.signal?.aborted) {
|
|
141
|
+
await new Promise((resolve) => {
|
|
142
|
+
wakeProducer = resolve;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return !consumerClosed && !options.signal?.aborted;
|
|
146
|
+
};
|
|
147
|
+
const reader = body.getReader();
|
|
148
|
+
const framer = createMatrxNdjsonFramer(options);
|
|
49
149
|
const onAbort = () => {
|
|
150
|
+
wakeWaitingProducer();
|
|
151
|
+
wakeWaitingConsumer();
|
|
50
152
|
void reader.cancel(options.signal?.reason).catch(() => void 0);
|
|
51
153
|
};
|
|
52
154
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
155
|
+
if (options.signal?.aborted) onAbort();
|
|
53
156
|
const readerPromise = (async () => {
|
|
54
|
-
let buffer = "";
|
|
55
157
|
try {
|
|
56
|
-
while (!options.signal?.aborted) {
|
|
158
|
+
while (!options.signal?.aborted && !consumerClosed) {
|
|
159
|
+
if (!await waitForReadCapacity()) return;
|
|
57
160
|
const { value, done } = await reader.read();
|
|
58
161
|
if (done) break;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
162
|
+
for (const envelope of framer.pushBytes(value)) {
|
|
163
|
+
if (!await enqueueEvent(envelope)) return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!options.signal?.aborted && !consumerClosed) {
|
|
167
|
+
for (const envelope of framer.finish()) {
|
|
168
|
+
if (!await enqueueEvent(envelope)) return;
|
|
169
|
+
}
|
|
63
170
|
}
|
|
64
|
-
buffer += decoder.decode();
|
|
65
|
-
if (!options.signal?.aborted && buffer.trim()) parseLine(buffer);
|
|
66
171
|
} catch (error) {
|
|
67
|
-
const aborted = options.signal?.aborted || error instanceof Error && error.name === "AbortError";
|
|
68
|
-
if (!aborted)
|
|
172
|
+
const aborted = options.signal?.aborted || consumerClosed || error instanceof Error && error.name === "AbortError";
|
|
173
|
+
if (!aborted) enqueueTerminal({ kind: "error", error });
|
|
69
174
|
} finally {
|
|
70
175
|
readerFinished = true;
|
|
71
176
|
reader.releaseLock();
|
|
72
|
-
|
|
177
|
+
enqueueTerminal({ kind: "done" });
|
|
73
178
|
}
|
|
74
179
|
})();
|
|
75
180
|
try {
|
|
76
181
|
while (true) {
|
|
77
182
|
if (queue.length === 0) {
|
|
183
|
+
if (options.signal?.aborted || readerFinished) return;
|
|
78
184
|
await new Promise((resolve) => {
|
|
79
185
|
wakeConsumer = resolve;
|
|
80
186
|
});
|
|
81
187
|
}
|
|
82
188
|
const item = queue.shift();
|
|
189
|
+
if (item?.kind === "event") queuedEventCount -= 1;
|
|
190
|
+
wakeWaitingProducer();
|
|
83
191
|
if (!item || item.kind === "done") return;
|
|
84
192
|
if (item.kind === "error") throw item.error;
|
|
85
193
|
yield item.value;
|
|
86
194
|
}
|
|
87
195
|
} finally {
|
|
196
|
+
consumerClosed = true;
|
|
197
|
+
wakeWaitingProducer();
|
|
198
|
+
wakeWaitingConsumer();
|
|
88
199
|
options.signal?.removeEventListener("abort", onAbort);
|
|
89
200
|
if (!readerFinished) {
|
|
90
201
|
await reader.cancel().catch(() => void 0);
|
|
@@ -300,7 +411,7 @@ function projectAgentEvent(current, event) {
|
|
|
300
411
|
const status = toolStatus(lifecycle);
|
|
301
412
|
return {
|
|
302
413
|
...next,
|
|
303
|
-
status: status === "
|
|
414
|
+
status: status === "delegated" ? "awaiting-tools" : status === "completed" || status === "error" ? "streaming" : next.status,
|
|
304
415
|
tools: {
|
|
305
416
|
...current.tools,
|
|
306
417
|
[callId]: {
|
|
@@ -357,7 +468,33 @@ function projectAgentEvents(initial, events) {
|
|
|
357
468
|
}
|
|
358
469
|
|
|
359
470
|
// projection/workflow.ts
|
|
360
|
-
var
|
|
471
|
+
var DEFAULT_WORKFLOW_PROJECTION_LIMITS = {
|
|
472
|
+
maxOpenFrameSets: 32,
|
|
473
|
+
maxFramesPerBlock: 256,
|
|
474
|
+
maxBytesPerBlock: 1048576
|
|
475
|
+
};
|
|
476
|
+
function positiveSafeInteger(name, value) {
|
|
477
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
478
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
479
|
+
}
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
function resolveLimits(limits) {
|
|
483
|
+
return {
|
|
484
|
+
maxOpenFrameSets: positiveSafeInteger(
|
|
485
|
+
"maxOpenFrameSets",
|
|
486
|
+
limits?.maxOpenFrameSets ?? DEFAULT_WORKFLOW_PROJECTION_LIMITS.maxOpenFrameSets
|
|
487
|
+
),
|
|
488
|
+
maxFramesPerBlock: positiveSafeInteger(
|
|
489
|
+
"maxFramesPerBlock",
|
|
490
|
+
limits?.maxFramesPerBlock ?? DEFAULT_WORKFLOW_PROJECTION_LIMITS.maxFramesPerBlock
|
|
491
|
+
),
|
|
492
|
+
maxBytesPerBlock: positiveSafeInteger(
|
|
493
|
+
"maxBytesPerBlock",
|
|
494
|
+
limits?.maxBytesPerBlock ?? DEFAULT_WORKFLOW_PROJECTION_LIMITS.maxBytesPerBlock
|
|
495
|
+
)
|
|
496
|
+
};
|
|
497
|
+
}
|
|
361
498
|
function createWorkflowNodeProjection(input) {
|
|
362
499
|
return {
|
|
363
500
|
runId: input.runId,
|
|
@@ -368,6 +505,8 @@ function createWorkflowNodeProjection(input) {
|
|
|
368
505
|
renderBlocks: {},
|
|
369
506
|
renderBlockOrder: [],
|
|
370
507
|
openFrames: {},
|
|
508
|
+
limits: resolveLimits(input.limits),
|
|
509
|
+
lastRenderBlockIssue: null,
|
|
371
510
|
chunksReceived: 0,
|
|
372
511
|
charsStreamed: 0,
|
|
373
512
|
lastStreamingTs: null,
|
|
@@ -386,6 +525,9 @@ function asString2(value) {
|
|
|
386
525
|
function asNumber2(value) {
|
|
387
526
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
388
527
|
}
|
|
528
|
+
function utf8ByteLength(value) {
|
|
529
|
+
return new TextEncoder().encode(value).byteLength;
|
|
530
|
+
}
|
|
389
531
|
function toRenderBlock(value) {
|
|
390
532
|
const data = asRecord2(value);
|
|
391
533
|
if (!data) return null;
|
|
@@ -403,44 +545,139 @@ function toRenderBlock(value) {
|
|
|
403
545
|
metadata: asRecord2(data.metadata)
|
|
404
546
|
};
|
|
405
547
|
}
|
|
548
|
+
function withIssue(next, event, code, frameId, message, openFrames = next.openFrames) {
|
|
549
|
+
return {
|
|
550
|
+
...next,
|
|
551
|
+
openFrames,
|
|
552
|
+
lastRenderBlockIssue: {
|
|
553
|
+
code,
|
|
554
|
+
frameId,
|
|
555
|
+
streamSeq: event.stream_seq,
|
|
556
|
+
message
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function withoutFrame(frames, frameId) {
|
|
561
|
+
if (!Object.hasOwn(frames, frameId)) return frames;
|
|
562
|
+
const next = { ...frames };
|
|
563
|
+
delete next[frameId];
|
|
564
|
+
return next;
|
|
565
|
+
}
|
|
406
566
|
function projectRenderFrame(current, event, next) {
|
|
407
567
|
const frameId = event.frame_id;
|
|
408
568
|
const frameCount = event.frame_count ?? 1;
|
|
409
569
|
const frameIndex = event.frame_index ?? 0;
|
|
410
|
-
if (!frameId || frameCount < 1 || frameIndex < 0 || frameIndex >= frameCount) {
|
|
411
|
-
return
|
|
570
|
+
if (!frameId || !Number.isSafeInteger(frameCount) || !Number.isSafeInteger(frameIndex) || frameCount < 1 || frameIndex < 0 || frameIndex >= frameCount) {
|
|
571
|
+
return withIssue(
|
|
572
|
+
next,
|
|
573
|
+
event,
|
|
574
|
+
"invalid_frame_metadata",
|
|
575
|
+
frameId ?? null,
|
|
576
|
+
"Render block frame metadata is invalid"
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (frameCount > current.limits.maxFramesPerBlock) {
|
|
580
|
+
return withIssue(
|
|
581
|
+
next,
|
|
582
|
+
event,
|
|
583
|
+
"frame_limit_exceeded",
|
|
584
|
+
frameId,
|
|
585
|
+
`Render block declares ${frameCount} frames; limit is ${current.limits.maxFramesPerBlock}`,
|
|
586
|
+
withoutFrame(current.openFrames, frameId)
|
|
587
|
+
);
|
|
412
588
|
}
|
|
413
589
|
const prior = current.openFrames[frameId];
|
|
590
|
+
if (prior && prior.frameCount !== frameCount) {
|
|
591
|
+
return withIssue(
|
|
592
|
+
next,
|
|
593
|
+
event,
|
|
594
|
+
"frame_count_mismatch",
|
|
595
|
+
frameId,
|
|
596
|
+
`Render block frame count changed from ${prior.frameCount} to ${frameCount}`,
|
|
597
|
+
withoutFrame(current.openFrames, frameId)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
const priorSlice = prior?.slices[frameIndex];
|
|
601
|
+
const byteLength = (prior?.byteLength ?? 0) - (priorSlice === void 0 ? 0 : utf8ByteLength(priorSlice)) + utf8ByteLength(event.delta);
|
|
602
|
+
if (byteLength > current.limits.maxBytesPerBlock) {
|
|
603
|
+
return withIssue(
|
|
604
|
+
next,
|
|
605
|
+
event,
|
|
606
|
+
"byte_limit_exceeded",
|
|
607
|
+
frameId,
|
|
608
|
+
`Render block exceeds ${current.limits.maxBytesPerBlock} UTF-8 bytes`,
|
|
609
|
+
withoutFrame(current.openFrames, frameId)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
414
612
|
const set = {
|
|
415
613
|
frameCount,
|
|
416
|
-
slices: { ...prior?.
|
|
614
|
+
slices: { ...prior?.slices ?? {}, [frameIndex]: event.delta },
|
|
615
|
+
byteLength
|
|
417
616
|
};
|
|
418
|
-
const openFrames = { ...current.openFrames
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
617
|
+
const openFrames = { ...current.openFrames };
|
|
618
|
+
let limitIssue = null;
|
|
619
|
+
if (!prior && Object.keys(openFrames).length >= current.limits.maxOpenFrameSets) {
|
|
620
|
+
const oldestFrameId = Object.keys(openFrames)[0];
|
|
621
|
+
if (oldestFrameId !== void 0) {
|
|
622
|
+
delete openFrames[oldestFrameId];
|
|
623
|
+
limitIssue = {
|
|
624
|
+
code: "open_frame_limit_exceeded",
|
|
625
|
+
frameId: oldestFrameId,
|
|
626
|
+
streamSeq: event.stream_seq,
|
|
627
|
+
message: `Evicted incomplete render block after reaching ${current.limits.maxOpenFrameSets} open frame sets`
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
openFrames[frameId] = set;
|
|
632
|
+
if (Object.keys(set.slices).length !== frameCount) {
|
|
633
|
+
return {
|
|
634
|
+
...next,
|
|
635
|
+
openFrames,
|
|
636
|
+
lastRenderBlockIssue: limitIssue ?? current.lastRenderBlockIssue
|
|
637
|
+
};
|
|
423
638
|
}
|
|
424
|
-
if (Object.keys(set.slices).length !== frameCount) return { ...next, openFrames };
|
|
425
639
|
delete openFrames[frameId];
|
|
426
|
-
|
|
640
|
+
const serialized = Array.from(
|
|
641
|
+
{ length: frameCount },
|
|
642
|
+
(_, index) => set.slices[index] ?? ""
|
|
643
|
+
).join("");
|
|
644
|
+
let parsed;
|
|
427
645
|
try {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
646
|
+
parsed = JSON.parse(serialized);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
649
|
+
return withIssue(
|
|
650
|
+
next,
|
|
651
|
+
event,
|
|
652
|
+
"malformed_json",
|
|
653
|
+
frameId,
|
|
654
|
+
`Completed render block is not valid JSON${detail}`,
|
|
655
|
+
openFrames
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const block = toRenderBlock(parsed);
|
|
659
|
+
if (!block) {
|
|
660
|
+
return withIssue(
|
|
661
|
+
next,
|
|
662
|
+
event,
|
|
663
|
+
"invalid_render_block",
|
|
664
|
+
frameId,
|
|
665
|
+
"Completed render block is missing blockId, blockIndex, or type",
|
|
666
|
+
openFrames
|
|
667
|
+
);
|
|
433
668
|
}
|
|
434
|
-
if (!block) return { ...next, openFrames };
|
|
435
669
|
return {
|
|
436
670
|
...next,
|
|
437
671
|
openFrames,
|
|
672
|
+
lastRenderBlockIssue: limitIssue ?? current.lastRenderBlockIssue,
|
|
438
673
|
renderBlocks: { ...current.renderBlocks, [block.blockId]: block },
|
|
439
674
|
renderBlockOrder: Object.hasOwn(current.renderBlocks, block.blockId) ? current.renderBlockOrder : [...current.renderBlockOrder, block.blockId]
|
|
440
675
|
};
|
|
441
676
|
}
|
|
442
677
|
function projectWorkflowNodeEvent(current, event) {
|
|
443
|
-
if (event.run_id !== current.runId || (event.node_id ?? "") !== current.nodeId)
|
|
678
|
+
if (event.run_id !== current.runId || (event.node_id ?? "") !== current.nodeId) {
|
|
679
|
+
return current;
|
|
680
|
+
}
|
|
444
681
|
if (event.stream_seq <= current.lastTransportSeq) return current;
|
|
445
682
|
const next = {
|
|
446
683
|
...current,
|
|
@@ -468,7 +705,10 @@ function projectWorkflowNodeEvent(current, event) {
|
|
|
468
705
|
}
|
|
469
706
|
}
|
|
470
707
|
|
|
708
|
+
exports.DEFAULT_MATRX_NDJSON_READ_AHEAD = DEFAULT_MATRX_NDJSON_READ_AHEAD;
|
|
709
|
+
exports.DEFAULT_WORKFLOW_PROJECTION_LIMITS = DEFAULT_WORKFLOW_PROJECTION_LIMITS;
|
|
471
710
|
exports.createAgentRequestProjection = createAgentRequestProjection;
|
|
711
|
+
exports.createMatrxNdjsonFramer = createMatrxNdjsonFramer;
|
|
472
712
|
exports.createWorkflowNodeProjection = createWorkflowNodeProjection;
|
|
473
713
|
exports.normalizeMatrxStreamEnvelope = normalizeMatrxStreamEnvelope;
|
|
474
714
|
exports.projectAgentEvent = projectAgentEvent;
|