agentfootprint-lens 0.19.0 → 0.21.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.
@@ -3,7 +3,11 @@ import {
3
3
  } from "./chunk-KQOLJKKM.js";
4
4
 
5
5
  // src/core/LensRecorder.ts
6
+ import { isDevMode } from "footprintjs";
6
7
  import { SequenceStore } from "footprintjs/trace";
8
+ import {
9
+ ALL_EVENT_TYPES
10
+ } from "agentfootprint";
7
11
  import { LiveStateRecorder, BoundaryRecorder } from "agentfootprint/observe";
8
12
  import {
9
13
  createTraceRuntimeOverlay
@@ -302,8 +306,10 @@ function relTime(runStartMs) {
302
306
  }
303
307
 
304
308
  // src/core/LensRecorder.ts
309
+ var DEFAULT_MAX_EVENTS = 5e4;
310
+ var KNOWN_EVENT_TYPES = new Set(ALL_EVENT_TYPES);
305
311
  var LensRecorder = class {
306
- constructor(rootLabel = "Run") {
312
+ constructor(rootLabel = "Run", options = {}) {
307
313
  /** Stable id for idempotent attach. */
308
314
  this.id = "lens";
309
315
  /** Composition: ordered + keyed event-log storage. */
@@ -374,6 +380,26 @@ var LensRecorder = class {
374
380
  * notifier. See `ChangeNotifier` JSDoc for adapter examples.
375
381
  */
376
382
  this.notifier = new ChangeNotifier();
383
+ /** Per-type count of events outside the agentfootprint registry.
384
+ * Always maintained (debug only gates console output). */
385
+ this.unknownEventTypes = /* @__PURE__ */ new Map();
386
+ /** Count of `popIfKind` bracket mismatches. Always maintained. */
387
+ this.bracketMismatchCount = 0;
388
+ /** Unknown types already warned about — warn ONCE per type, not per
389
+ * event, so a chatty unknown emitter can't flood the console. */
390
+ this.warnedUnknownTypes = /* @__PURE__ */ new Set();
391
+ /** Entries evicted by the cap so far. Surfaced via `getDiagnostics()`. */
392
+ this.droppedEventCount = 0;
393
+ /** Eviction already warned about — warn ONCE per run, not per batch. */
394
+ this.warnedEviction = false;
395
+ this.debug = options.debug;
396
+ const cap = options.maxEvents ?? DEFAULT_MAX_EVENTS;
397
+ if (cap !== Number.POSITIVE_INFINITY && (!Number.isInteger(cap) || cap < 1)) {
398
+ throw new RangeError(
399
+ `LensRecorder: maxEvents must be a positive integer or Infinity, got ${cap}`
400
+ );
401
+ }
402
+ this.maxEvents = cap;
377
403
  this.root = {
378
404
  id: "run-root",
379
405
  kind: "run",
@@ -400,11 +426,50 @@ var LensRecorder = class {
400
426
  this.finalStatus = "running";
401
427
  this.runError = void 0;
402
428
  this.lastRunId = void 0;
429
+ this.unknownEventTypes.clear();
430
+ this.bracketMismatchCount = 0;
431
+ this.warnedUnknownTypes.clear();
432
+ this.droppedEventCount = 0;
433
+ this.warnedEviction = false;
403
434
  this.liveState.clear();
404
435
  this.boundary.clear();
405
436
  this.runtime.reset();
406
437
  this.bumpVersion();
407
438
  }
439
+ /**
440
+ * Health counters for the observed event stream (backlog item U4).
441
+ * Always maintained — no debug flag needed — so UIs and tests can
442
+ * assert stream health without scraping the console:
443
+ *
444
+ * - `unknownEventTypes` — per-type counts of events whose `type` is
445
+ * not in agentfootprint's event registry (e.g. a newer
446
+ * agentfootprint emitting types this lens doesn't know, or a
447
+ * custom dispatcher leaking foreign events). These events are
448
+ * still attached to the current top node — counted, not dropped.
449
+ * - `bracketMismatches` — close events (`llm_end`, `tool_end`,
450
+ * `composition.exit`, ...) whose kind didn't match the top of the
451
+ * build stack (malformed ordering). The close is skipped; the
452
+ * tree stays partially structured rather than crashing.
453
+ * - `droppedEvents` — entries evicted by the `maxEvents` FIFO cap
454
+ * (U3). When non-zero, log-derived views cover only the retained
455
+ * tail of the run.
456
+ *
457
+ * All are `{}` / `0` on a well-formed run that stayed under the cap.
458
+ * Reset by `clear()`. Returns a fresh snapshot object on every call.
459
+ */
460
+ getDiagnostics() {
461
+ return {
462
+ unknownEventTypes: Object.fromEntries(this.unknownEventTypes),
463
+ bracketMismatches: this.bracketMismatchCount,
464
+ droppedEvents: this.droppedEventCount
465
+ };
466
+ }
467
+ /** Whether diagnostic warnings go to the console: explicit option
468
+ * wins; otherwise follow footprintjs's global dev-mode flag
469
+ * (evaluated per event so `enableDevMode()` mid-run takes effect). */
470
+ debugEnabled() {
471
+ return this.debug ?? isDevMode();
472
+ }
408
473
  /**
409
474
  * The StepGraph the UI renders — agentfootprint's ReAct projection
410
475
  * (actor-arrow steps with `iterationIndex` + `slotUpdated`, plus
@@ -555,13 +620,73 @@ var LensRecorder = class {
555
620
  };
556
621
  this.store.push(entry);
557
622
  this.top().events.push(entry);
623
+ this.noteUnknownType(event.type);
558
624
  this.dispatch(event, runOffsetMs, entry);
625
+ this.enforceCap();
559
626
  this.bumpVersion();
560
627
  }
628
+ /**
629
+ * U3 — enforce the `maxEvents` FIFO cap. When the store exceeds the
630
+ * cap, evict the oldest entries down to ~90% of the cap in ONE batch
631
+ * (amortized O(1) per event: one O(retained) rebuild per ~10%-of-cap
632
+ * pushes), then prune the SAME evicted entries from every run-tree
633
+ * node's `events` list — entry objects are shared references, so
634
+ * skipping the tree would hide the memory, not release it.
635
+ *
636
+ * `SequenceStore` is append-only by design (no removal API), so the
637
+ * batch rebuild (clear + re-push retained) is the supported eviction
638
+ * path; the per-step key + range indices rebuild correctly during
639
+ * re-push.
640
+ */
641
+ enforceCap() {
642
+ if (this.store.size <= this.maxEvents) return;
643
+ const all = this.store.getAll();
644
+ const evictBatch = Math.max(1, Math.floor(this.maxEvents / 10));
645
+ const retainCount = Math.max(1, this.maxEvents - evictBatch);
646
+ const dropCount = all.length - retainCount;
647
+ const retained = all.slice(dropCount);
648
+ this.store.clear();
649
+ for (const e of retained) this.store.push(e);
650
+ this.droppedEventCount += dropCount;
651
+ this.pruneNodeEvents(this.root, retained[0].seq);
652
+ if (this.debugEnabled() && !this.warnedEviction) {
653
+ this.warnedEviction = true;
654
+ console.warn(
655
+ `[lens] LensRecorder: maxEvents cap (${this.maxEvents}) reached \u2014 evicting oldest events (FIFO, ~10% per batch). Log-derived views now cover only the retained tail; evicted total in getDiagnostics().droppedEvents. Raise the cap via lensRecorder('Run', { maxEvents }) or pass Infinity to disable. (Warned once.)`
656
+ );
657
+ }
658
+ }
659
+ /** Drop entries with `seq < minSeq` from a node's `events` list (and
660
+ * its descendants'). Per-node lists are seq-ordered, so this is a
661
+ * prefix splice — in place, preserving the node object identity the
662
+ * build stack may still hold. */
663
+ pruneNodeEvents(node, minSeq) {
664
+ if (node.events.length > 0 && node.events[0].seq < minSeq) {
665
+ let keepFrom = 0;
666
+ while (keepFrom < node.events.length && node.events[keepFrom].seq < minSeq) keepFrom++;
667
+ node.events.splice(0, keepFrom);
668
+ }
669
+ for (const child of node.children) this.pruneNodeEvents(child, minSeq);
670
+ }
561
671
  /** Notify all subscribers + bump version. Delegated to ChangeNotifier. */
562
672
  bumpVersion() {
563
673
  this.notifier.notify();
564
674
  }
675
+ /**
676
+ * U4 diagnostics — count (and, in debug, warn ONCE per type about)
677
+ * event types outside agentfootprint's registry. One Set lookup per
678
+ * event on the happy path.
679
+ */
680
+ noteUnknownType(type) {
681
+ if (KNOWN_EVENT_TYPES.has(type)) return;
682
+ this.unknownEventTypes.set(type, (this.unknownEventTypes.get(type) ?? 0) + 1);
683
+ if (this.debugEnabled() && !this.warnedUnknownTypes.has(type)) {
684
+ this.warnedUnknownTypes.add(type);
685
+ console.warn(
686
+ `[lens] LensRecorder: unknown event type '${type}' \u2014 not in agentfootprint's event registry. Attached to the current node without structural handling. (Warned once per type; counts in getDiagnostics().unknownEventTypes.)`
687
+ );
688
+ }
689
+ }
565
690
  /**
566
691
  * Kind-specific handling. Keeps the switch exhaustive over every v2
567
692
  * event type we structurally care about; the default branch is the
@@ -586,10 +711,14 @@ var LensRecorder = class {
586
711
  }
587
712
  if (type === "agentfootprint.composition.exit") {
588
713
  const p = event.payload;
589
- this.popIfKind("composition", {
590
- endOffsetMs: runOffsetMs,
591
- status: p.status === "ok" ? "ok" : p.status === "budget_exhausted" ? "budget_exhausted" : "err"
592
- });
714
+ this.popIfKind(
715
+ "composition",
716
+ {
717
+ endOffsetMs: runOffsetMs,
718
+ status: p.status === "ok" ? "ok" : p.status === "budget_exhausted" ? "budget_exhausted" : "err"
719
+ },
720
+ entry.runtimeStageId
721
+ );
593
722
  return;
594
723
  }
595
724
  if (type === "agentfootprint.composition.iteration_start") {
@@ -608,11 +737,15 @@ var LensRecorder = class {
608
737
  }
609
738
  if (type === "agentfootprint.composition.iteration_exit") {
610
739
  const p = event.payload;
611
- this.popIfKind("iteration", {
612
- endOffsetMs: runOffsetMs,
613
- status: p.reason === "budget" ? "budget_exhausted" : "ok",
614
- iterationExit: p.reason
615
- });
740
+ this.popIfKind(
741
+ "iteration",
742
+ {
743
+ endOffsetMs: runOffsetMs,
744
+ status: p.reason === "budget" ? "budget_exhausted" : "ok",
745
+ iterationExit: p.reason
746
+ },
747
+ entry.runtimeStageId
748
+ );
616
749
  return;
617
750
  }
618
751
  if (type === "agentfootprint.agent.turn_start") {
@@ -629,7 +762,7 @@ var LensRecorder = class {
629
762
  return;
630
763
  }
631
764
  if (type === "agentfootprint.agent.turn_end") {
632
- this.popIfKind("iteration", { endOffsetMs: runOffsetMs, status: "ok" });
765
+ this.popIfKind("iteration", { endOffsetMs: runOffsetMs, status: "ok" }, entry.runtimeStageId);
633
766
  return;
634
767
  }
635
768
  if (type === "agentfootprint.agent.iteration_start") {
@@ -647,7 +780,7 @@ var LensRecorder = class {
647
780
  return;
648
781
  }
649
782
  if (type === "agentfootprint.agent.iteration_end") {
650
- this.popIfKind("iteration", { endOffsetMs: runOffsetMs, status: "ok" });
783
+ this.popIfKind("iteration", { endOffsetMs: runOffsetMs, status: "ok" }, entry.runtimeStageId);
651
784
  return;
652
785
  }
653
786
  if (type === "agentfootprint.stream.llm_start") {
@@ -674,16 +807,20 @@ var LensRecorder = class {
674
807
  }
675
808
  if (type === "agentfootprint.stream.llm_end") {
676
809
  const p = event.payload;
677
- this.popIfKind("llm-call", {
678
- endOffsetMs: runOffsetMs,
679
- status: "ok",
680
- llmEnd: {
681
- content: p.content,
682
- toolCallCount: p.toolCallCount,
683
- usage: p.usage,
684
- stopReason: p.stopReason
685
- }
686
- });
810
+ this.popIfKind(
811
+ "llm-call",
812
+ {
813
+ endOffsetMs: runOffsetMs,
814
+ status: "ok",
815
+ llmEnd: {
816
+ content: p.content,
817
+ toolCallCount: p.toolCallCount,
818
+ usage: p.usage,
819
+ stopReason: p.stopReason
820
+ }
821
+ },
822
+ entry.runtimeStageId
823
+ );
687
824
  return;
688
825
  }
689
826
  if (type === "agentfootprint.stream.tool_start") {
@@ -708,11 +845,15 @@ var LensRecorder = class {
708
845
  }
709
846
  if (type === "agentfootprint.stream.tool_end") {
710
847
  const p = event.payload;
711
- this.popIfKind("tool-call", {
712
- endOffsetMs: runOffsetMs,
713
- status: p.error === true ? "err" : "ok",
714
- toolEnd: { result: p.result, error: p.error ?? false }
715
- });
848
+ this.popIfKind(
849
+ "tool-call",
850
+ {
851
+ endOffsetMs: runOffsetMs,
852
+ status: p.error === true ? "err" : "ok",
853
+ toolEnd: { result: p.result, error: p.error ?? false }
854
+ },
855
+ entry.runtimeStageId
856
+ );
716
857
  return;
717
858
  }
718
859
  if (type === "agentfootprint.pause.request") {
@@ -748,12 +889,23 @@ var LensRecorder = class {
748
889
  }
749
890
  /**
750
891
  * Pop the top node IF its kind matches, applying finalization fields.
751
- * Mismatched kinds (indicating malformed event ordering) are logged
752
- * but don't throw — Lens prefers partial correctness to crashes.
892
+ * Mismatched kinds (indicating malformed event ordering) are SKIPPED,
893
+ * never thrown — Lens prefers partial correctness to crashes. Every
894
+ * mismatch increments `getDiagnostics().bracketMismatches` (U4), and
895
+ * when debug is on (`LensRecorderOptions.debug` or footprintjs
896
+ * `isDevMode()`) each mismatch logs a `console.warn` with the
897
+ * expected vs found kind plus the closing event's `runtimeStageId`.
898
+ * Well-formed runs stay console-silent either way.
753
899
  */
754
- popIfKind(kind, finalize) {
900
+ popIfKind(kind, finalize, runtimeStageId) {
755
901
  const top = this.top();
756
902
  if (top.kind !== kind) {
903
+ this.bracketMismatchCount += 1;
904
+ if (this.debugEnabled()) {
905
+ console.warn(
906
+ `[lens] LensRecorder: bracket mismatch \u2014 tried to close a '${kind}' node but the top of the stack is '${top.kind}'` + (runtimeStageId !== void 0 ? ` (runtimeStageId: ${runtimeStageId})` : "") + `. Close event skipped; the tree stays partially structured.`
907
+ );
908
+ }
757
909
  return;
758
910
  }
759
911
  top.endOffsetMs = finalize.endOffsetMs;
@@ -814,7 +966,13 @@ var LensRecorder = class {
814
966
  }
815
967
  /** Summary stats — computed lazily via `store.aggregate()`.
816
968
  * Single-pass fold; types derived from the AgentfootprintEvent
817
- * discriminated union. */
969
+ * discriminated union.
970
+ *
971
+ * U3 caveat: once the `maxEvents` cap has evicted entries
972
+ * (`getDiagnostics().droppedEvents > 0`), the folded counts/tokens
973
+ * reflect only RETAINED events. `startedAt` / `durationMs` stay
974
+ * anchored to the true first event of the run (tracked outside the
975
+ * store), so the time axis never shifts. */
818
976
  selectSummary() {
819
977
  const init = {
820
978
  llmCallCount: 0,
@@ -854,7 +1012,7 @@ var LensRecorder = class {
854
1012
  }
855
1013
  }, init);
856
1014
  const entries = this.store.getAll();
857
- const startedAt = entries[0]?.wallClockMs ?? 0;
1015
+ const startedAt = this.runStartMs ?? entries[0]?.wallClockMs ?? 0;
858
1016
  const endedAt = entries[entries.length - 1]?.wallClockMs;
859
1017
  return {
860
1018
  startedAt,
@@ -909,8 +1067,8 @@ function buildDetails(n) {
909
1067
  }
910
1068
  return void 0;
911
1069
  }
912
- function lensRecorder(rootLabel) {
913
- return new LensRecorder(rootLabel);
1070
+ function lensRecorder(rootLabel, options) {
1071
+ return new LensRecorder(rootLabel, options);
914
1072
  }
915
1073
 
916
1074
  // src/core/buildStepGraphFromSnapshot.ts
@@ -1618,7 +1776,7 @@ function makeEdge(kind, source, target, options = {}) {
1618
1776
  }
1619
1777
 
1620
1778
  // src/core/translate/helpers/mergeOutputs.ts
1621
- import { isDevMode } from "footprintjs";
1779
+ import { isDevMode as isDevMode2 } from "footprintjs";
1622
1780
  function mergeOutputs(outputs, rootNodeId) {
1623
1781
  const nodes = [];
1624
1782
  const edges = [];
@@ -1626,7 +1784,7 @@ function mergeOutputs(outputs, rootNodeId) {
1626
1784
  for (const n of o.nodes) nodes.push(n);
1627
1785
  for (const e of o.edges) edges.push(e);
1628
1786
  }
1629
- if (isDevMode()) assertNoCollisions(nodes, edges);
1787
+ if (isDevMode2()) assertNoCollisions(nodes, edges);
1630
1788
  return { nodes, edges, rootNodeId };
1631
1789
  }
1632
1790
  function assertNoCollisions(nodes, edges) {
@@ -2258,6 +2416,7 @@ export {
2258
2416
  ChangeNotifier,
2259
2417
  lensSnapshotRecorder,
2260
2418
  LensSnapshotRecorder,
2419
+ DEFAULT_MAX_EVENTS,
2261
2420
  LensRecorder,
2262
2421
  lensRecorder,
2263
2422
  buildStepGraphFromSnapshot,
@@ -2295,4 +2454,4 @@ export {
2295
2454
  defaultSize,
2296
2455
  layoutLensGraph
2297
2456
  };
2298
- //# sourceMappingURL=chunk-A2ELAEZX.js.map
2457
+ //# sourceMappingURL=chunk-BTVAAE66.js.map