@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/dist/index.js CHANGED
@@ -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);
@@ -298,7 +409,7 @@ function projectAgentEvent(current, event) {
298
409
  const status = toolStatus(lifecycle);
299
410
  return {
300
411
  ...next,
301
- status: status === "started" || status === "delegated" ? "awaiting-tools" : status === "completed" || status === "error" ? "streaming" : next.status,
412
+ status: status === "delegated" ? "awaiting-tools" : status === "completed" || status === "error" ? "streaming" : next.status,
302
413
  tools: {
303
414
  ...current.tools,
304
415
  [callId]: {
@@ -355,7 +466,33 @@ function projectAgentEvents(initial, events) {
355
466
  }
356
467
 
357
468
  // projection/workflow.ts
358
- var MAX_OPEN_FRAME_SETS = 32;
469
+ var DEFAULT_WORKFLOW_PROJECTION_LIMITS = {
470
+ maxOpenFrameSets: 32,
471
+ maxFramesPerBlock: 256,
472
+ maxBytesPerBlock: 1048576
473
+ };
474
+ function positiveSafeInteger(name, value) {
475
+ if (!Number.isSafeInteger(value) || value < 1) {
476
+ throw new RangeError(`${name} must be a positive safe integer`);
477
+ }
478
+ return value;
479
+ }
480
+ function resolveLimits(limits) {
481
+ return {
482
+ maxOpenFrameSets: positiveSafeInteger(
483
+ "maxOpenFrameSets",
484
+ limits?.maxOpenFrameSets ?? DEFAULT_WORKFLOW_PROJECTION_LIMITS.maxOpenFrameSets
485
+ ),
486
+ maxFramesPerBlock: positiveSafeInteger(
487
+ "maxFramesPerBlock",
488
+ limits?.maxFramesPerBlock ?? DEFAULT_WORKFLOW_PROJECTION_LIMITS.maxFramesPerBlock
489
+ ),
490
+ maxBytesPerBlock: positiveSafeInteger(
491
+ "maxBytesPerBlock",
492
+ limits?.maxBytesPerBlock ?? DEFAULT_WORKFLOW_PROJECTION_LIMITS.maxBytesPerBlock
493
+ )
494
+ };
495
+ }
359
496
  function createWorkflowNodeProjection(input) {
360
497
  return {
361
498
  runId: input.runId,
@@ -366,6 +503,8 @@ function createWorkflowNodeProjection(input) {
366
503
  renderBlocks: {},
367
504
  renderBlockOrder: [],
368
505
  openFrames: {},
506
+ limits: resolveLimits(input.limits),
507
+ lastRenderBlockIssue: null,
369
508
  chunksReceived: 0,
370
509
  charsStreamed: 0,
371
510
  lastStreamingTs: null,
@@ -384,6 +523,9 @@ function asString2(value) {
384
523
  function asNumber2(value) {
385
524
  return typeof value === "number" && Number.isFinite(value) ? value : null;
386
525
  }
526
+ function utf8ByteLength(value) {
527
+ return new TextEncoder().encode(value).byteLength;
528
+ }
387
529
  function toRenderBlock(value) {
388
530
  const data = asRecord2(value);
389
531
  if (!data) return null;
@@ -401,44 +543,139 @@ function toRenderBlock(value) {
401
543
  metadata: asRecord2(data.metadata)
402
544
  };
403
545
  }
546
+ function withIssue(next, event, code, frameId, message, openFrames = next.openFrames) {
547
+ return {
548
+ ...next,
549
+ openFrames,
550
+ lastRenderBlockIssue: {
551
+ code,
552
+ frameId,
553
+ streamSeq: event.stream_seq,
554
+ message
555
+ }
556
+ };
557
+ }
558
+ function withoutFrame(frames, frameId) {
559
+ if (!Object.hasOwn(frames, frameId)) return frames;
560
+ const next = { ...frames };
561
+ delete next[frameId];
562
+ return next;
563
+ }
404
564
  function projectRenderFrame(current, event, next) {
405
565
  const frameId = event.frame_id;
406
566
  const frameCount = event.frame_count ?? 1;
407
567
  const frameIndex = event.frame_index ?? 0;
408
- if (!frameId || frameCount < 1 || frameIndex < 0 || frameIndex >= frameCount) {
409
- return next;
568
+ if (!frameId || !Number.isSafeInteger(frameCount) || !Number.isSafeInteger(frameIndex) || frameCount < 1 || frameIndex < 0 || frameIndex >= frameCount) {
569
+ return withIssue(
570
+ next,
571
+ event,
572
+ "invalid_frame_metadata",
573
+ frameId ?? null,
574
+ "Render block frame metadata is invalid"
575
+ );
576
+ }
577
+ if (frameCount > current.limits.maxFramesPerBlock) {
578
+ return withIssue(
579
+ next,
580
+ event,
581
+ "frame_limit_exceeded",
582
+ frameId,
583
+ `Render block declares ${frameCount} frames; limit is ${current.limits.maxFramesPerBlock}`,
584
+ withoutFrame(current.openFrames, frameId)
585
+ );
410
586
  }
411
587
  const prior = current.openFrames[frameId];
588
+ if (prior && prior.frameCount !== frameCount) {
589
+ return withIssue(
590
+ next,
591
+ event,
592
+ "frame_count_mismatch",
593
+ frameId,
594
+ `Render block frame count changed from ${prior.frameCount} to ${frameCount}`,
595
+ withoutFrame(current.openFrames, frameId)
596
+ );
597
+ }
598
+ const priorSlice = prior?.slices[frameIndex];
599
+ const byteLength = (prior?.byteLength ?? 0) - (priorSlice === void 0 ? 0 : utf8ByteLength(priorSlice)) + utf8ByteLength(event.delta);
600
+ if (byteLength > current.limits.maxBytesPerBlock) {
601
+ return withIssue(
602
+ next,
603
+ event,
604
+ "byte_limit_exceeded",
605
+ frameId,
606
+ `Render block exceeds ${current.limits.maxBytesPerBlock} UTF-8 bytes`,
607
+ withoutFrame(current.openFrames, frameId)
608
+ );
609
+ }
412
610
  const set = {
413
611
  frameCount,
414
- slices: { ...prior?.frameCount === frameCount ? prior.slices : {}, [frameIndex]: event.delta }
612
+ slices: { ...prior?.slices ?? {}, [frameIndex]: event.delta },
613
+ byteLength
415
614
  };
416
- const openFrames = { ...current.openFrames, [frameId]: set };
417
- const keys = Object.keys(openFrames);
418
- const oldestKey = keys[0];
419
- if (keys.length > MAX_OPEN_FRAME_SETS && oldestKey !== void 0) {
420
- delete openFrames[oldestKey];
615
+ const openFrames = { ...current.openFrames };
616
+ let limitIssue = null;
617
+ if (!prior && Object.keys(openFrames).length >= current.limits.maxOpenFrameSets) {
618
+ const oldestFrameId = Object.keys(openFrames)[0];
619
+ if (oldestFrameId !== void 0) {
620
+ delete openFrames[oldestFrameId];
621
+ limitIssue = {
622
+ code: "open_frame_limit_exceeded",
623
+ frameId: oldestFrameId,
624
+ streamSeq: event.stream_seq,
625
+ message: `Evicted incomplete render block after reaching ${current.limits.maxOpenFrameSets} open frame sets`
626
+ };
627
+ }
628
+ }
629
+ openFrames[frameId] = set;
630
+ if (Object.keys(set.slices).length !== frameCount) {
631
+ return {
632
+ ...next,
633
+ openFrames,
634
+ lastRenderBlockIssue: limitIssue ?? current.lastRenderBlockIssue
635
+ };
421
636
  }
422
- if (Object.keys(set.slices).length !== frameCount) return { ...next, openFrames };
423
637
  delete openFrames[frameId];
424
- let block = null;
638
+ const serialized = Array.from(
639
+ { length: frameCount },
640
+ (_, index) => set.slices[index] ?? ""
641
+ ).join("");
642
+ let parsed;
425
643
  try {
426
- block = toRenderBlock(JSON.parse(
427
- Array.from({ length: frameCount }, (_, index) => set.slices[index] ?? "").join("")
428
- ));
429
- } catch {
430
- return { ...next, openFrames };
644
+ parsed = JSON.parse(serialized);
645
+ } catch (error) {
646
+ const detail = error instanceof Error ? `: ${error.message}` : "";
647
+ return withIssue(
648
+ next,
649
+ event,
650
+ "malformed_json",
651
+ frameId,
652
+ `Completed render block is not valid JSON${detail}`,
653
+ openFrames
654
+ );
655
+ }
656
+ const block = toRenderBlock(parsed);
657
+ if (!block) {
658
+ return withIssue(
659
+ next,
660
+ event,
661
+ "invalid_render_block",
662
+ frameId,
663
+ "Completed render block is missing blockId, blockIndex, or type",
664
+ openFrames
665
+ );
431
666
  }
432
- if (!block) return { ...next, openFrames };
433
667
  return {
434
668
  ...next,
435
669
  openFrames,
670
+ lastRenderBlockIssue: limitIssue ?? current.lastRenderBlockIssue,
436
671
  renderBlocks: { ...current.renderBlocks, [block.blockId]: block },
437
672
  renderBlockOrder: Object.hasOwn(current.renderBlocks, block.blockId) ? current.renderBlockOrder : [...current.renderBlockOrder, block.blockId]
438
673
  };
439
674
  }
440
675
  function projectWorkflowNodeEvent(current, event) {
441
- if (event.run_id !== current.runId || (event.node_id ?? "") !== current.nodeId) return current;
676
+ if (event.run_id !== current.runId || (event.node_id ?? "") !== current.nodeId) {
677
+ return current;
678
+ }
442
679
  if (event.stream_seq <= current.lastTransportSeq) return current;
443
680
  const next = {
444
681
  ...current,
@@ -466,6 +703,6 @@ function projectWorkflowNodeEvent(current, event) {
466
703
  }
467
704
  }
468
705
 
469
- export { createAgentRequestProjection, createWorkflowNodeProjection, normalizeMatrxStreamEnvelope, projectAgentEvent, projectAgentEvents, projectAgentResultForDisplay, projectWorkflowNodeEvent, readMatrxNdjsonStream };
706
+ export { DEFAULT_MATRX_NDJSON_READ_AHEAD, DEFAULT_WORKFLOW_PROJECTION_LIMITS, createAgentRequestProjection, createMatrxNdjsonFramer, createWorkflowNodeProjection, normalizeMatrxStreamEnvelope, projectAgentEvent, projectAgentEvents, projectAgentResultForDisplay, projectWorkflowNodeEvent, readMatrxNdjsonStream };
470
707
  //# sourceMappingURL=index.js.map
471
708
  //# sourceMappingURL=index.js.map