agentfootprint 2.14.4 → 3.0.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.
Files changed (224) hide show
  1. package/dist/cache/CacheDecisionSubflow.js +3 -1
  2. package/dist/cache/CacheDecisionSubflow.js.map +1 -1
  3. package/dist/conventions.js +3 -2
  4. package/dist/conventions.js.map +1 -1
  5. package/dist/core/Agent.js +69 -23
  6. package/dist/core/Agent.js.map +1 -1
  7. package/dist/core/LLMCall.js +44 -5
  8. package/dist/core/LLMCall.js.map +1 -1
  9. package/dist/core/RunnerBase.js +173 -0
  10. package/dist/core/RunnerBase.js.map +1 -1
  11. package/dist/core/agent/buildAgentChart.js +12 -2
  12. package/dist/core/agent/buildAgentChart.js.map +1 -1
  13. package/dist/core/runner.js +4 -3
  14. package/dist/core/runner.js.map +1 -1
  15. package/dist/core/slots/buildMessagesSlot.js +1 -1
  16. package/dist/core/slots/buildMessagesSlot.js.map +1 -1
  17. package/dist/core/slots/buildSystemPromptSlot.js +1 -1
  18. package/dist/core/slots/buildSystemPromptSlot.js.map +1 -1
  19. package/dist/core/slots/buildThinkingSubflow.js +1 -1
  20. package/dist/core/slots/buildThinkingSubflow.js.map +1 -1
  21. package/dist/core/slots/buildToolsSlot.js +3 -1
  22. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  23. package/dist/core/translator.js +32 -0
  24. package/dist/core/translator.js.map +1 -0
  25. package/dist/core-flow/Conditional.js +73 -10
  26. package/dist/core-flow/Conditional.js.map +1 -1
  27. package/dist/core-flow/Loop.js +60 -16
  28. package/dist/core-flow/Loop.js.map +1 -1
  29. package/dist/core-flow/Parallel.js +241 -92
  30. package/dist/core-flow/Parallel.js.map +1 -1
  31. package/dist/core-flow/Sequence.js +51 -8
  32. package/dist/core-flow/Sequence.js.map +1 -1
  33. package/dist/esm/cache/CacheDecisionSubflow.js +3 -1
  34. package/dist/esm/cache/CacheDecisionSubflow.js.map +1 -1
  35. package/dist/esm/conventions.js +3 -2
  36. package/dist/esm/conventions.js.map +1 -1
  37. package/dist/esm/core/Agent.js +69 -23
  38. package/dist/esm/core/Agent.js.map +1 -1
  39. package/dist/esm/core/LLMCall.js +44 -5
  40. package/dist/esm/core/LLMCall.js.map +1 -1
  41. package/dist/esm/core/RunnerBase.js +173 -0
  42. package/dist/esm/core/RunnerBase.js.map +1 -1
  43. package/dist/esm/core/agent/buildAgentChart.js +12 -2
  44. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  45. package/dist/esm/core/runner.js +4 -3
  46. package/dist/esm/core/runner.js.map +1 -1
  47. package/dist/esm/core/slots/buildMessagesSlot.js +1 -1
  48. package/dist/esm/core/slots/buildMessagesSlot.js.map +1 -1
  49. package/dist/esm/core/slots/buildSystemPromptSlot.js +1 -1
  50. package/dist/esm/core/slots/buildSystemPromptSlot.js.map +1 -1
  51. package/dist/esm/core/slots/buildThinkingSubflow.js +1 -1
  52. package/dist/esm/core/slots/buildThinkingSubflow.js.map +1 -1
  53. package/dist/esm/core/slots/buildToolsSlot.js +3 -1
  54. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  55. package/dist/esm/core/translator.js +31 -0
  56. package/dist/esm/core/translator.js.map +1 -0
  57. package/dist/esm/core-flow/Conditional.js +73 -10
  58. package/dist/esm/core-flow/Conditional.js.map +1 -1
  59. package/dist/esm/core-flow/Loop.js +60 -16
  60. package/dist/esm/core-flow/Loop.js.map +1 -1
  61. package/dist/esm/core-flow/Parallel.js +242 -93
  62. package/dist/esm/core-flow/Parallel.js.map +1 -1
  63. package/dist/esm/core-flow/Sequence.js +51 -8
  64. package/dist/esm/core-flow/Sequence.js.map +1 -1
  65. package/dist/esm/index.js +1 -0
  66. package/dist/esm/index.js.map +1 -1
  67. package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js +1 -1
  68. package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
  69. package/dist/esm/memory/causal/snapshotPipeline.js +6 -2
  70. package/dist/esm/memory/causal/snapshotPipeline.js.map +1 -1
  71. package/dist/esm/memory/pipeline/auto.js +2 -2
  72. package/dist/esm/memory/pipeline/auto.js.map +1 -1
  73. package/dist/esm/memory/pipeline/default.js +4 -2
  74. package/dist/esm/memory/pipeline/default.js.map +1 -1
  75. package/dist/esm/memory/pipeline/ephemeral.js +3 -1
  76. package/dist/esm/memory/pipeline/ephemeral.js.map +1 -1
  77. package/dist/esm/memory/pipeline/fact.js +4 -2
  78. package/dist/esm/memory/pipeline/fact.js.map +1 -1
  79. package/dist/esm/memory/pipeline/narrative.js +4 -2
  80. package/dist/esm/memory/pipeline/narrative.js.map +1 -1
  81. package/dist/esm/memory/pipeline/semantic.js +2 -2
  82. package/dist/esm/memory/pipeline/semantic.js.map +1 -1
  83. package/dist/esm/observe.js +1 -1
  84. package/dist/esm/observe.js.map +1 -1
  85. package/dist/esm/patterns/MapReduce.js +5 -5
  86. package/dist/esm/patterns/MapReduce.js.map +1 -1
  87. package/dist/esm/patterns/Swarm.js +1 -1
  88. package/dist/esm/patterns/Swarm.js.map +1 -1
  89. package/dist/esm/recorders/observability/BoundaryRecorder.js +315 -36
  90. package/dist/esm/recorders/observability/BoundaryRecorder.js.map +1 -1
  91. package/dist/esm/recorders/observability/FlowchartRecorder.js +10 -0
  92. package/dist/esm/recorders/observability/FlowchartRecorder.js.map +1 -1
  93. package/dist/esm/recorders/observability/LiveStateRecorder.js +112 -21
  94. package/dist/esm/recorders/observability/LiveStateRecorder.js.map +1 -1
  95. package/dist/esm/recorders/observability/RunStepRecorder.js +652 -0
  96. package/dist/esm/recorders/observability/RunStepRecorder.js.map +1 -0
  97. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +3 -4
  98. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  99. package/dist/esm/recorders/observability/internal/ActorArrowClassifier.js +34 -0
  100. package/dist/esm/recorders/observability/internal/ActorArrowClassifier.js.map +1 -0
  101. package/dist/esm/recorders/observability/internal/CandidateAnswerBuffer.js +32 -0
  102. package/dist/esm/recorders/observability/internal/CandidateAnswerBuffer.js.map +1 -0
  103. package/dist/esm/recorders/observability/internal/ForkTracker.js +84 -0
  104. package/dist/esm/recorders/observability/internal/ForkTracker.js.map +1 -0
  105. package/dist/esm/recorders/observability/internal/RootInferrer.js +114 -0
  106. package/dist/esm/recorders/observability/internal/RootInferrer.js.map +1 -0
  107. package/dist/esm/recorders/observability/internal/SequenceSiblingTracker.js +31 -0
  108. package/dist/esm/recorders/observability/internal/SequenceSiblingTracker.js.map +1 -0
  109. package/dist/esm/recorders/observability/observeRunId.js +21 -0
  110. package/dist/esm/recorders/observability/observeRunId.js.map +1 -0
  111. package/dist/esm/reliability/buildReliabilityGateChart.js +1 -1
  112. package/dist/esm/reliability/buildReliabilityGateChart.js.map +1 -1
  113. package/dist/index.js +7 -3
  114. package/dist/index.js.map +1 -1
  115. package/dist/lib/injection-engine/buildInjectionEngineSubflow.js +1 -1
  116. package/dist/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
  117. package/dist/memory/causal/snapshotPipeline.js +6 -2
  118. package/dist/memory/causal/snapshotPipeline.js.map +1 -1
  119. package/dist/memory/pipeline/auto.js +2 -2
  120. package/dist/memory/pipeline/auto.js.map +1 -1
  121. package/dist/memory/pipeline/default.js +4 -2
  122. package/dist/memory/pipeline/default.js.map +1 -1
  123. package/dist/memory/pipeline/ephemeral.js +3 -1
  124. package/dist/memory/pipeline/ephemeral.js.map +1 -1
  125. package/dist/memory/pipeline/fact.js +4 -2
  126. package/dist/memory/pipeline/fact.js.map +1 -1
  127. package/dist/memory/pipeline/narrative.js +4 -2
  128. package/dist/memory/pipeline/narrative.js.map +1 -1
  129. package/dist/memory/pipeline/semantic.js +2 -2
  130. package/dist/memory/pipeline/semantic.js.map +1 -1
  131. package/dist/observe.js +1 -1
  132. package/dist/observe.js.map +1 -1
  133. package/dist/patterns/MapReduce.js +5 -5
  134. package/dist/patterns/MapReduce.js.map +1 -1
  135. package/dist/patterns/Swarm.js +1 -1
  136. package/dist/patterns/Swarm.js.map +1 -1
  137. package/dist/recorders/observability/BoundaryRecorder.js +314 -35
  138. package/dist/recorders/observability/BoundaryRecorder.js.map +1 -1
  139. package/dist/recorders/observability/FlowchartRecorder.js +10 -0
  140. package/dist/recorders/observability/FlowchartRecorder.js.map +1 -1
  141. package/dist/recorders/observability/LiveStateRecorder.js +111 -20
  142. package/dist/recorders/observability/LiveStateRecorder.js.map +1 -1
  143. package/dist/recorders/observability/RunStepRecorder.js +658 -0
  144. package/dist/recorders/observability/RunStepRecorder.js.map +1 -0
  145. package/dist/recorders/observability/commentary/commentaryTemplates.js +3 -4
  146. package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  147. package/dist/recorders/observability/internal/ActorArrowClassifier.js +38 -0
  148. package/dist/recorders/observability/internal/ActorArrowClassifier.js.map +1 -0
  149. package/dist/recorders/observability/internal/CandidateAnswerBuffer.js +36 -0
  150. package/dist/recorders/observability/internal/CandidateAnswerBuffer.js.map +1 -0
  151. package/dist/recorders/observability/internal/ForkTracker.js +88 -0
  152. package/dist/recorders/observability/internal/ForkTracker.js.map +1 -0
  153. package/dist/recorders/observability/internal/RootInferrer.js +118 -0
  154. package/dist/recorders/observability/internal/RootInferrer.js.map +1 -0
  155. package/dist/recorders/observability/internal/SequenceSiblingTracker.js +35 -0
  156. package/dist/recorders/observability/internal/SequenceSiblingTracker.js.map +1 -0
  157. package/dist/recorders/observability/observeRunId.js +25 -0
  158. package/dist/recorders/observability/observeRunId.js.map +1 -0
  159. package/dist/reliability/buildReliabilityGateChart.js +1 -1
  160. package/dist/reliability/buildReliabilityGateChart.js.map +1 -1
  161. package/dist/types/cache/CacheDecisionSubflow.d.ts.map +1 -1
  162. package/dist/types/conventions.d.ts.map +1 -1
  163. package/dist/types/core/Agent.d.ts +20 -18
  164. package/dist/types/core/Agent.d.ts.map +1 -1
  165. package/dist/types/core/LLMCall.d.ts +28 -2
  166. package/dist/types/core/LLMCall.d.ts.map +1 -1
  167. package/dist/types/core/RunnerBase.d.ts +124 -4
  168. package/dist/types/core/RunnerBase.d.ts.map +1 -1
  169. package/dist/types/core/agent/buildAgentChart.d.ts +7 -1
  170. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  171. package/dist/types/core/agent/types.d.ts +29 -0
  172. package/dist/types/core/agent/types.d.ts.map +1 -1
  173. package/dist/types/core/runner.d.ts +51 -5
  174. package/dist/types/core/runner.d.ts.map +1 -1
  175. package/dist/types/core/slots/buildMessagesSlot.d.ts.map +1 -1
  176. package/dist/types/core/slots/buildSystemPromptSlot.d.ts.map +1 -1
  177. package/dist/types/core/slots/buildThinkingSubflow.d.ts.map +1 -1
  178. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  179. package/dist/types/core/translator.d.ts +95 -0
  180. package/dist/types/core/translator.d.ts.map +1 -0
  181. package/dist/types/core-flow/Conditional.d.ts +48 -4
  182. package/dist/types/core-flow/Conditional.d.ts.map +1 -1
  183. package/dist/types/core-flow/Loop.d.ts +42 -3
  184. package/dist/types/core-flow/Loop.d.ts.map +1 -1
  185. package/dist/types/core-flow/Parallel.d.ts +99 -4
  186. package/dist/types/core-flow/Parallel.d.ts.map +1 -1
  187. package/dist/types/core-flow/Sequence.d.ts +49 -3
  188. package/dist/types/core-flow/Sequence.d.ts.map +1 -1
  189. package/dist/types/events/payloads.d.ts +22 -1
  190. package/dist/types/events/payloads.d.ts.map +1 -1
  191. package/dist/types/index.d.ts +4 -2
  192. package/dist/types/index.d.ts.map +1 -1
  193. package/dist/types/lib/injection-engine/buildInjectionEngineSubflow.d.ts.map +1 -1
  194. package/dist/types/memory/causal/snapshotPipeline.d.ts.map +1 -1
  195. package/dist/types/memory/pipeline/auto.d.ts.map +1 -1
  196. package/dist/types/memory/pipeline/default.d.ts.map +1 -1
  197. package/dist/types/memory/pipeline/ephemeral.d.ts.map +1 -1
  198. package/dist/types/memory/pipeline/fact.d.ts.map +1 -1
  199. package/dist/types/memory/pipeline/narrative.d.ts.map +1 -1
  200. package/dist/types/memory/pipeline/semantic.d.ts.map +1 -1
  201. package/dist/types/observe.d.ts +2 -2
  202. package/dist/types/observe.d.ts.map +1 -1
  203. package/dist/types/recorders/observability/BoundaryRecorder.d.ts +160 -6
  204. package/dist/types/recorders/observability/BoundaryRecorder.d.ts.map +1 -1
  205. package/dist/types/recorders/observability/FlowchartRecorder.d.ts.map +1 -1
  206. package/dist/types/recorders/observability/LiveStateRecorder.d.ts +42 -6
  207. package/dist/types/recorders/observability/LiveStateRecorder.d.ts.map +1 -1
  208. package/dist/types/recorders/observability/RunStepRecorder.d.ts +232 -0
  209. package/dist/types/recorders/observability/RunStepRecorder.d.ts.map +1 -0
  210. package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
  211. package/dist/types/recorders/observability/internal/ActorArrowClassifier.d.ts +26 -0
  212. package/dist/types/recorders/observability/internal/ActorArrowClassifier.d.ts.map +1 -0
  213. package/dist/types/recorders/observability/internal/CandidateAnswerBuffer.d.ts +29 -0
  214. package/dist/types/recorders/observability/internal/CandidateAnswerBuffer.d.ts.map +1 -0
  215. package/dist/types/recorders/observability/internal/ForkTracker.d.ts +61 -0
  216. package/dist/types/recorders/observability/internal/ForkTracker.d.ts.map +1 -0
  217. package/dist/types/recorders/observability/internal/RootInferrer.d.ts +52 -0
  218. package/dist/types/recorders/observability/internal/RootInferrer.d.ts.map +1 -0
  219. package/dist/types/recorders/observability/internal/SequenceSiblingTracker.d.ts +25 -0
  220. package/dist/types/recorders/observability/internal/SequenceSiblingTracker.d.ts.map +1 -0
  221. package/dist/types/recorders/observability/observeRunId.d.ts +37 -0
  222. package/dist/types/recorders/observability/observeRunId.d.ts.map +1 -0
  223. package/dist/types/reliability/buildReliabilityGateChart.d.ts.map +1 -1
  224. package/package.json +3 -3
@@ -0,0 +1,658 @@
1
+ "use strict";
2
+ /**
3
+ * RunStepRecorder — slider-ready ordered list of RunSteps, BUILT
4
+ * INCREMENTALLY during traversal. Real-time recorder, not a walker.
5
+ *
6
+ * Pattern: extends `SequenceRecorder<RunStep>` (shared storage shelf)
7
+ * and implements `CombinedRecorder` (FlowRecorder hooks).
8
+ * Subscribes to the agentfootprint typed-event dispatcher
9
+ * for actor-arrow events. Each event handler decides whether
10
+ * to emit a step; state lives on the instance and persists
11
+ * across the run.
12
+ * Role: The single source of truth for "what slider positions
13
+ * exist in this run, and what transitions does each light
14
+ * up." Lens consumers attach the recorder once and read
15
+ * `getSteps()` — no per-render re-derivation.
16
+ *
17
+ * Why this matters: the older `buildRunSteps(events)` walker violated
18
+ * footprintjs's core principle ("collect during traversal, never
19
+ * post-process"). Each call walked the full event log multiple times;
20
+ * the playground triggered a full walk on every flowchart update,
21
+ * yielding O(N²) total work for a streaming run. The recorder pattern
22
+ * is O(N) — one handler call per event — and matches BoundaryRecorder /
23
+ * FlowchartRecorder / KeyedRecorder idioms throughout the library.
24
+ *
25
+ * The `buildRunSteps(...)` function is RETAINED as a thin compatibility
26
+ * shim that constructs a fresh recorder, replays events through it,
27
+ * and returns the resulting entries. Useful for snapshot-from-saved-
28
+ * events use cases (replay, testing, post-hoc analysis). Live consumers
29
+ * should attach the recorder directly via `runner.attach(rec)`.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.buildRunSteps = exports.RunStepRecorder = exports.runStepRecorder = void 0;
33
+ const trace_1 = require("footprintjs/trace");
34
+ const observeRunId_js_1 = require("./observeRunId.js");
35
+ const ForkTracker_js_1 = require("./internal/ForkTracker.js");
36
+ const SequenceSiblingTracker_js_1 = require("./internal/SequenceSiblingTracker.js");
37
+ const CandidateAnswerBuffer_js_1 = require("./internal/CandidateAnswerBuffer.js");
38
+ const RootInferrer_js_1 = require("./internal/RootInferrer.js");
39
+ const ActorArrowClassifier_js_1 = require("./internal/ActorArrowClassifier.js");
40
+ let _counter = 0;
41
+ /** Factory — matches the `boundaryRecorder()` / `topologyRecorder()` style. */
42
+ function runStepRecorder(options = {}) {
43
+ return new RunStepRecorder(options);
44
+ }
45
+ exports.runStepRecorder = runStepRecorder;
46
+ // ─── Constants ──────────────────────────────────────────────────────
47
+ const ACTOR_USER = 'actor:user';
48
+ const LEAF_PRIMITIVES = new Set(['Agent', 'LLMCall']);
49
+ function isLeafPrimitive(kind) {
50
+ return kind !== undefined && LEAF_PRIMITIVES.has(kind);
51
+ }
52
+ function lastSegment(subflowId) {
53
+ const i = subflowId.lastIndexOf('/');
54
+ return i >= 0 ? subflowId.slice(i + 1) : subflowId;
55
+ }
56
+ function isPathPrefix(prefix, path) {
57
+ if (prefix.length > path.length)
58
+ return false;
59
+ for (let i = 0; i < prefix.length; i++) {
60
+ if (prefix[i] !== path[i])
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+ function pathFromCtx(s) {
66
+ const segments = s ? s.split('/').filter(Boolean) : [];
67
+ return [trace_1.ROOT_SUBFLOW_ID, ...segments];
68
+ }
69
+ // ─── Recorder class ────────────────────────────────────────────────
70
+ /**
71
+ * Real-time slider-step recorder. Emits a `RunStep` whenever an event
72
+ * marks a meaningful slider transition. State persists on the instance
73
+ * so successive events update bookkeeping in O(1).
74
+ *
75
+ * Attach via `runner.attach(rec)` for FlowRecorder events; call
76
+ * `rec.subscribe(runner.dispatcher)` for actor-arrow events. The
77
+ * `getSteps(drillPath?)` method returns the already-built list (no
78
+ * walking) with optional drill-scope filtering.
79
+ */
80
+ class RunStepRecorder {
81
+ id;
82
+ /** Composition: storage shelf for the slider-step sequence. */
83
+ store = new trace_1.SequenceStore();
84
+ /** Run-boundary observer — fires this.clear() when traversalContext.runId
85
+ * changes between events. THIS IS THE FIX for the Parallel multi-run
86
+ * aliasing bug — without it `forkKey = ${parent}@${rid}` collides
87
+ * because rid resets to `seed#0` on each run. */
88
+ runIdGuard = (0, observeRunId_js_1.createRunIdObserver)(() => this.resetForNewRun());
89
+ // ── Composed sub-trackers (one concern each) ─────────────────────
90
+ /** Stack of currently-open boundaries. The recorder owns this
91
+ * directly because it's a simple stack and frames are recorder-
92
+ * shaped. */
93
+ boundaryStack = [];
94
+ /** Fork-emission coalescing + branch-exit tally. */
95
+ forks = new ForkTracker_js_1.ForkTracker();
96
+ /** Tracks the most-recent leaf exit per depth → "forwards" handoff. */
97
+ siblings = new SequenceSiblingTracker_js_1.SequenceSiblingTracker();
98
+ /** Buffers a "this MIGHT be the answer" leaf until onRunEnd. */
99
+ answerBuffer = new CandidateAnswerBuffer_js_1.CandidateAnswerBuffer();
100
+ /** Run-root inference state machine (leaf vs composition). */
101
+ rootInferrer = new RootInferrer_js_1.RootInferrer();
102
+ /** llm.start / llm.end actor-arrow classifier. */
103
+ actorArrows = new ActorArrowClassifier_js_1.ActorArrowClassifier();
104
+ /** Has the first `asks` step fired? */
105
+ asksEmitted = false;
106
+ constructor(options = {}) {
107
+ this.id = options.id ?? `run-step-${++_counter}`;
108
+ }
109
+ /**
110
+ * Emit a RunStep, auto-mirroring `anchor.runtimeStageId` to the
111
+ * top-level `runtimeStageId` field that the keyed index uses. Single
112
+ * source of truth (the anchor) — never inconsistent with the storage
113
+ * key.
114
+ */
115
+ push(step) {
116
+ this.store.push({ ...step, runtimeStageId: step.anchor.runtimeStageId });
117
+ }
118
+ /** Internal seq-numbering helper — mirrors the store size so each
119
+ * RunStep gets a unique 0-based index in emit order. */
120
+ get entryCount() {
121
+ return this.store.size;
122
+ }
123
+ clear() {
124
+ this.resetForNewRun();
125
+ this.runIdGuard.reset();
126
+ }
127
+ /** Internal — wipe all per-run state WITHOUT resetting the runIdGuard
128
+ * itself. Called by `clear()` (which then resets the guard) AND by
129
+ * the runIdGuard's onNewRun callback (where the guard is mid-update
130
+ * and must NOT be reset, only the recorder's data should be).
131
+ *
132
+ * Note: each sub-tracker owns its OWN clear; the orchestrator just
133
+ * fans out. Adding new state to a sub-tracker requires no edit here. */
134
+ resetForNewRun() {
135
+ this.store.clear();
136
+ this.boundaryStack = [];
137
+ this.forks.clear();
138
+ this.siblings.clear();
139
+ this.answerBuffer.clear();
140
+ this.rootInferrer.clear();
141
+ this.actorArrows.clear();
142
+ this.asksEmitted = false;
143
+ }
144
+ observeRunId(runId) {
145
+ this.runIdGuard.observe(runId);
146
+ }
147
+ // ── FlowRecorder hooks ─────────────────────────────────────────
148
+ onRunStart(event) {
149
+ this.observeRunId(event.traversalContext?.runId);
150
+ // Nothing to emit yet — the first leaf entry / fork event starts
151
+ // the slider.
152
+ }
153
+ onRunEnd(event) {
154
+ this.observeRunId(event.traversalContext?.runId);
155
+ // Emit the deferred `answers` step for the last leaf exit at run
156
+ // scope. Without this, runs with a Sequence-rooted shape never
157
+ // see their final answer reflected on the slider.
158
+ this.flushCandidateAnswer();
159
+ }
160
+ onSubflowEntry(event) {
161
+ if (!event.subflowId)
162
+ return;
163
+ this.observeRunId(event.traversalContext?.runId);
164
+ const ctx = event.traversalContext;
165
+ const runtimeStageId = ctx?.runtimeStageId ?? '';
166
+ const subflowPath = pathFromCtx(ctx?.subflowPath);
167
+ const depth = subflowPath.length - 1;
168
+ const ts = Date.now();
169
+ const subflowId = event.subflowId;
170
+ const description = event.description;
171
+ const primitiveKind = parsePrimitiveKind(description);
172
+ // Delegate root inference to the dedicated state machine.
173
+ this.rootInferrer.observeSubflowEntry(depth, primitiveKind);
174
+ const frame = {
175
+ subflowId,
176
+ subflowPath,
177
+ ...(primitiveKind ? { primitiveKind } : {}),
178
+ depth,
179
+ };
180
+ const isLeaf = isLeafPrimitive(primitiveKind);
181
+ if (this.forks.isForkChild(subflowId)) {
182
+ // Branch wrapper of a tracked fork — push frame so the matching
183
+ // exit can tally for the merge step. No sequential emission.
184
+ this.boundaryStack.push(frame);
185
+ return;
186
+ }
187
+ if (isLeaf) {
188
+ const prevSibling = this.siblings.peekPrevSibling(depth);
189
+ if (prevSibling) {
190
+ // Sequence-style sibling handoff: previous leaf exited, new
191
+ // leaf entered at the same depth → emit `forwards`.
192
+ this.push({
193
+ seq: this.entryCount,
194
+ kind: 'sequential',
195
+ transitions: [{ from: prevSibling, to: subflowId, via: 'next', label: 'forwards' }],
196
+ anchor: { runtimeStageId, subflowPath },
197
+ label: 'forwards',
198
+ tsMs: ts,
199
+ });
200
+ }
201
+ else if (!this.asksEmitted) {
202
+ // First leaf in the run — User asks it.
203
+ this.push({
204
+ seq: this.entryCount,
205
+ kind: 'sequential',
206
+ transitions: [{ from: ACTOR_USER, to: subflowId, via: 'next', label: 'asks' }],
207
+ anchor: { runtimeStageId, subflowPath },
208
+ label: 'asks',
209
+ tsMs: ts,
210
+ });
211
+ this.asksEmitted = true;
212
+ }
213
+ }
214
+ this.boundaryStack.push(frame);
215
+ }
216
+ onSubflowExit(event) {
217
+ this.observeRunId(event.traversalContext?.runId);
218
+ const ctx = event.traversalContext;
219
+ const runtimeStageId = ctx?.runtimeStageId ?? '';
220
+ const ts = Date.now();
221
+ const frame = this.boundaryStack.pop();
222
+ if (!frame)
223
+ return;
224
+ const isLeaf = isLeafPrimitive(frame.primitiveKind);
225
+ // Fork-branch exit: ForkTracker tallies completion and signals
226
+ // when ALL branches have exited so we can emit the merge step.
227
+ const merge = this.forks.recordChildExit(frame.subflowId);
228
+ if (merge) {
229
+ this.push({
230
+ seq: this.entryCount,
231
+ kind: 'merge',
232
+ transitions: merge.branches.map((sid) => ({
233
+ from: sid,
234
+ to: ACTOR_USER,
235
+ via: 'next',
236
+ label: lastSegment(sid),
237
+ })),
238
+ anchor: { runtimeStageId, subflowPath: frame.subflowPath },
239
+ label: `merge (${merge.branches.length})`,
240
+ tsMs: ts,
241
+ meta: { kind: 'merge', mergedCount: merge.branches.length },
242
+ });
243
+ return;
244
+ }
245
+ // If it WAS a fork child but not the last to exit, still skip the
246
+ // leaf-handoff path below.
247
+ if (this.forks.isForkChild(frame.subflowId))
248
+ return;
249
+ if (isLeaf) {
250
+ // Sibling handoff + answer-candidate buffering. Both delegated.
251
+ this.siblings.recordExit(frame.depth, frame.subflowId);
252
+ this.answerBuffer.set(frame, ts, runtimeStageId);
253
+ }
254
+ }
255
+ onFork(event) {
256
+ this.observeRunId(event.traversalContext?.runId);
257
+ const ctx = event.traversalContext;
258
+ const runtimeStageId = ctx?.runtimeStageId ?? '';
259
+ const subflowPath = pathFromCtx(ctx?.subflowPath);
260
+ const ts = Date.now();
261
+ const depth = subflowPath.length - 1;
262
+ // ForkTracker handles registration + dedup. Cross-run collisions
263
+ // (Committee + TolerantCommittee back-to-back, both emitting
264
+ // `seed#0` as the parent stage) are prevented by observeRunId
265
+ // above — the tracker's state is wiped when runId changes.
266
+ const reg = this.forks.registerFork(event.parent, runtimeStageId, event.children);
267
+ if (!reg.fresh)
268
+ return;
269
+ // Fork at depth 0 = Parallel root signal.
270
+ this.rootInferrer.observeFork(depth);
271
+ const branches = [...event.children];
272
+ this.push({
273
+ seq: this.entryCount,
274
+ kind: 'fork',
275
+ transitions: branches.map((childName) => ({
276
+ from: ACTOR_USER,
277
+ to: childName,
278
+ via: 'fork-branch',
279
+ label: childName,
280
+ })),
281
+ anchor: { runtimeStageId, subflowPath },
282
+ label: `fork (${branches.length})`,
283
+ tsMs: ts,
284
+ meta: { kind: 'fork', parentSubflowId: event.parent },
285
+ });
286
+ }
287
+ onDecision(event) {
288
+ this.observeRunId(event.traversalContext?.runId);
289
+ const ctx = event.traversalContext;
290
+ // Skip Agent-internal decisions — those are encoded by the actor
291
+ // arrows that follow.
292
+ const stageId = ctx?.stageId ?? '';
293
+ const { localStageId } = (0, trace_1.splitStageId)(stageId);
294
+ if (isAgentInternalStageId(localStageId))
295
+ return;
296
+ const runtimeStageId = ctx?.runtimeStageId ?? '';
297
+ const subflowPath = pathFromCtx(ctx?.subflowPath);
298
+ const ts = Date.now();
299
+ const depth = subflowPath.length - 1;
300
+ this.rootInferrer.observeDecision(depth);
301
+ this.push({
302
+ seq: this.entryCount,
303
+ kind: 'decide',
304
+ transitions: [
305
+ {
306
+ from: runtimeStageId,
307
+ to: event.chosen,
308
+ via: 'decision-branch',
309
+ label: event.chosen,
310
+ },
311
+ ],
312
+ anchor: { runtimeStageId, subflowPath },
313
+ label: `routes to ${event.chosen}`,
314
+ tsMs: ts,
315
+ meta: {
316
+ kind: 'decide',
317
+ chosen: event.chosen,
318
+ ...(event.rationale ? { rationale: event.rationale } : {}),
319
+ },
320
+ });
321
+ }
322
+ onLoop(event) {
323
+ this.observeRunId(event.traversalContext?.runId);
324
+ const ctx = event.traversalContext;
325
+ const runtimeStageId = ctx?.runtimeStageId ?? '';
326
+ const subflowPath = pathFromCtx(ctx?.subflowPath);
327
+ const ts = Date.now();
328
+ const depth = subflowPath.length - 1;
329
+ this.rootInferrer.observeLoop(depth);
330
+ this.push({
331
+ seq: this.entryCount,
332
+ kind: 'iteration',
333
+ transitions: [
334
+ {
335
+ from: event.target,
336
+ to: event.target,
337
+ via: 'loop-iteration',
338
+ label: `iter ${event.iteration}`,
339
+ },
340
+ ],
341
+ anchor: { runtimeStageId, subflowPath },
342
+ label: `iter ${event.iteration}`,
343
+ tsMs: ts,
344
+ meta: { kind: 'iteration', index: event.iteration, target: event.target },
345
+ });
346
+ }
347
+ // ── Typed-event subscription (actor arrows) ──────────────────────
348
+ /**
349
+ * Subscribe to the runner's typed-event dispatcher and emit a
350
+ * `react` RunStep on every `llm.start` / `llm.end`. The recorder
351
+ * classifies `actorArrow` locally (mirrors BoundaryRecorder's
352
+ * pattern) so consumers don't have to depend on BoundaryRecorder's
353
+ * own subscription order.
354
+ */
355
+ subscribe(dispatcher) {
356
+ return dispatcher.on('*', (event) => this.ingestTypedEvent(event));
357
+ }
358
+ /** Internal — also called by `ingestDomainEvent` for shim replay.
359
+ *
360
+ * NOTE: deliberately does NOT call observeRunId(event.meta.runId).
361
+ * The agentfootprint dispatcher's runId is a DIFFERENT generator
362
+ * than footprintjs's traversalContext.runId — mixing them would
363
+ * toggle lastRunId on every event and trigger a false reset.
364
+ * Run-boundary detection happens reliably on the FlowRecorder side
365
+ * (onRunStart fires FIRST in any new run before any typed event). */
366
+ ingestTypedEvent(event) {
367
+ if (event.type === 'agentfootprint.stream.llm_start') {
368
+ const meta = event.meta;
369
+ const runtimeStageId = meta.runtimeStageId ?? '';
370
+ const subflowPath = [trace_1.ROOT_SUBFLOW_ID, ...(meta.subflowPath ?? [])];
371
+ const ts = meta.wallClockMs;
372
+ const arrow = this.actorArrows.classifyStart();
373
+ const llmStage = `stage:llm:${runtimeStageId}`;
374
+ const from = arrow === 'tool→llm' ? `stage:tool:${runtimeStageId}` : ACTOR_USER;
375
+ this.push({
376
+ seq: this.entryCount,
377
+ kind: 'react',
378
+ transitions: [{ from, to: llmStage, via: 'actor-arrow', label: arrow }],
379
+ anchor: { runtimeStageId, subflowPath },
380
+ label: arrow,
381
+ tsMs: ts,
382
+ meta: { kind: 'react', actorArrow: arrow },
383
+ });
384
+ }
385
+ else if (event.type === 'agentfootprint.stream.llm_end') {
386
+ const p = event.payload;
387
+ const meta = event.meta;
388
+ const runtimeStageId = meta.runtimeStageId ?? '';
389
+ const subflowPath = [trace_1.ROOT_SUBFLOW_ID, ...(meta.subflowPath ?? [])];
390
+ const ts = meta.wallClockMs;
391
+ const arrow = this.actorArrows.classifyEnd(p.toolCallCount);
392
+ const llmStage = `stage:llm:${runtimeStageId}`;
393
+ const to = arrow === 'llm→user' ? ACTOR_USER : `stage:tool:${runtimeStageId}`;
394
+ this.push({
395
+ seq: this.entryCount,
396
+ kind: 'react',
397
+ transitions: [{ from: llmStage, to, via: 'actor-arrow', label: arrow }],
398
+ anchor: { runtimeStageId, subflowPath },
399
+ label: arrow,
400
+ tsMs: ts,
401
+ meta: { kind: 'react', actorArrow: arrow },
402
+ });
403
+ }
404
+ }
405
+ // ── Replay API (for shim / tests / offline analysis) ─────────────
406
+ /**
407
+ * Feed a single recorded `DomainEvent` (from BoundaryRecorder) into
408
+ * this recorder as if it had fired live. Used by `buildRunSteps`
409
+ * for snapshot replay; tests use it for fixture-driven projection.
410
+ *
411
+ * Live consumers should use `runner.attach(rec)` +
412
+ * `rec.subscribe(dispatcher)` instead — the recorder's hooks fire
413
+ * naturally during traversal.
414
+ */
415
+ ingestDomainEvent(e) {
416
+ const traversalContext = {
417
+ runId: 'replay',
418
+ stageId: lastSegment(e.subflowPath.join('/') || trace_1.ROOT_SUBFLOW_ID),
419
+ runtimeStageId: e.runtimeStageId,
420
+ stageName: lastSegment(e.subflowPath.join('/') || trace_1.ROOT_SUBFLOW_ID),
421
+ depth: e.depth,
422
+ ...(e.subflowPath.length > 1 ? { subflowPath: e.subflowPath.slice(1).join('/') } : {}),
423
+ };
424
+ switch (e.type) {
425
+ case 'run.entry':
426
+ this.onRunStart({ payload: e.payload, traversalContext });
427
+ break;
428
+ case 'run.exit':
429
+ this.onRunEnd({ payload: e.payload, traversalContext });
430
+ break;
431
+ case 'subflow.entry':
432
+ this.onSubflowEntry({
433
+ name: e.subflowName,
434
+ subflowId: e.subflowId,
435
+ ...(e.description ? { description: e.description } : {}),
436
+ traversalContext,
437
+ });
438
+ break;
439
+ case 'subflow.exit':
440
+ this.onSubflowExit({
441
+ name: e.subflowName,
442
+ subflowId: e.subflowId,
443
+ traversalContext,
444
+ });
445
+ break;
446
+ case 'fork.branch':
447
+ // Replay layer; coalescing is handled at the call-site
448
+ // (`buildRunSteps`) which groups events by parent+ts and
449
+ // calls `onFork` once. A single fork.branch slipping through
450
+ // to here is treated as a 1-child fork — degenerate but
451
+ // harmless.
452
+ this.onFork({
453
+ parent: e.parentSubflowId,
454
+ children: [e.childName],
455
+ traversalContext,
456
+ });
457
+ break;
458
+ case 'decision.branch':
459
+ this.onDecision({
460
+ decider: e.decider,
461
+ chosen: e.chosen,
462
+ ...(e.rationale ? { rationale: e.rationale } : {}),
463
+ traversalContext,
464
+ });
465
+ break;
466
+ case 'loop.iteration':
467
+ this.onLoop({
468
+ target: e.target,
469
+ iteration: e.iteration,
470
+ traversalContext,
471
+ });
472
+ break;
473
+ case 'llm.start': {
474
+ // Synthesize a typed event matching the dispatcher payload
475
+ // shape so `ingestTypedEvent` produces the same react step.
476
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
477
+ this.ingestTypedEvent({
478
+ type: 'agentfootprint.stream.llm_start',
479
+ payload: {
480
+ provider: e.provider,
481
+ model: e.model,
482
+ ...(e.systemPromptChars !== undefined
483
+ ? { systemPromptChars: e.systemPromptChars }
484
+ : {}),
485
+ ...(e.messagesCount !== undefined ? { messagesCount: e.messagesCount } : {}),
486
+ ...(e.toolsCount !== undefined ? { toolsCount: e.toolsCount } : {}),
487
+ },
488
+ meta: {
489
+ wallClockMs: e.ts,
490
+ runOffsetMs: 0,
491
+ runtimeStageId: e.runtimeStageId,
492
+ // Strip the ROOT_SUBFLOW_ID prefix for the typed-event
493
+ // convention (typed events use the inner path, BoundaryRecorder
494
+ // adds the root prefix on ingest).
495
+ subflowPath: e.subflowPath.slice(1),
496
+ compositionPath: [],
497
+ runId: 'replay',
498
+ },
499
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
500
+ });
501
+ break;
502
+ }
503
+ case 'llm.end': {
504
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
505
+ this.ingestTypedEvent({
506
+ type: 'agentfootprint.stream.llm_end',
507
+ payload: {
508
+ content: e.content,
509
+ toolCallCount: e.toolCallCount,
510
+ usage: e.usage,
511
+ ...(e.stopReason ? { stopReason: e.stopReason } : {}),
512
+ },
513
+ meta: {
514
+ wallClockMs: e.ts,
515
+ runOffsetMs: 0,
516
+ runtimeStageId: e.runtimeStageId,
517
+ subflowPath: e.subflowPath.slice(1),
518
+ compositionPath: [],
519
+ runId: 'replay',
520
+ },
521
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
522
+ });
523
+ break;
524
+ }
525
+ // tool.start / tool.end / context.injected — not currently used
526
+ // by the projection (tools are paired with the next llm.start;
527
+ // context injections decorate steps but don't add new ones).
528
+ default:
529
+ break;
530
+ }
531
+ }
532
+ // ── Query API ─────────────────────────────────────────────────
533
+ /**
534
+ * Read-only query — returns the already-built step list filtered to
535
+ * `drillPath` scope. O(1) per call when scope is empty; O(N) filter
536
+ * otherwise. Composition-vs-leaf root filter is applied so the
537
+ * slider semantics match the user's mental model:
538
+ *
539
+ * - **Leaf root** (single Agent / LLMCall): show `react` steps only.
540
+ * - **Composition root** (Sequence / Parallel / Conditional / Loop):
541
+ * show composition steps; hide intra-leaf `react` steps.
542
+ *
543
+ * Drill-down filters by `anchor.subflowPath` prefix and re-applies
544
+ * the leaf-vs-composition rule for the drilled scope.
545
+ */
546
+ getSteps(drillPath) {
547
+ const all = this.store.getAll();
548
+ const path = drillPath ?? [];
549
+ // Leaf-vs-composition filter — delegated to RootInferrer.
550
+ const isLeafRoot = this.rootInferrer.isLeafRoot();
551
+ const kindFiltered = isLeafRoot
552
+ ? all.filter((s) => s.kind === 'react')
553
+ : all.filter((s) => s.kind !== 'react');
554
+ if (path.length === 0) {
555
+ return reseq(kindFiltered);
556
+ }
557
+ return reseq(kindFiltered.filter((s) => isPathPrefix(path, s.anchor.subflowPath)));
558
+ }
559
+ // ── Helpers ───────────────────────────────────────────────────
560
+ /** Flush any deferred answer-candidate from the buffer. Called by
561
+ * `onRunEnd` so a single `answers` step appears for runs that end
562
+ * on a leaf exit (no further leaf entries followed). */
563
+ flushCandidateAnswer() {
564
+ const c = this.answerBuffer.flush();
565
+ if (!c)
566
+ return;
567
+ this.push({
568
+ seq: this.entryCount,
569
+ kind: 'sequential',
570
+ transitions: [{ from: c.frame.subflowId, to: ACTOR_USER, via: 'next', label: 'answers' }],
571
+ anchor: {
572
+ runtimeStageId: c.runtimeStageId,
573
+ subflowPath: c.frame.subflowPath,
574
+ },
575
+ label: 'answers',
576
+ tsMs: c.tsMs,
577
+ });
578
+ }
579
+ }
580
+ exports.RunStepRecorder = RunStepRecorder;
581
+ // ─── Helpers ─────────────────────────────────────────────────────
582
+ function parsePrimitiveKind(description) {
583
+ if (!description)
584
+ return undefined;
585
+ const colon = description.indexOf(':');
586
+ if (colon < 0)
587
+ return undefined;
588
+ const prefix = description.slice(0, colon).trim();
589
+ return prefix.length > 0 ? prefix : undefined;
590
+ }
591
+ const AGENT_INTERNAL_STAGES = new Set(['route', 'tool-calls', 'final', 'merge']);
592
+ function isAgentInternalStageId(localStageId) {
593
+ return AGENT_INTERNAL_STAGES.has(localStageId);
594
+ }
595
+ /** Renumber `seq` after filtering so consumers see contiguous indices. */
596
+ function reseq(steps) {
597
+ return steps.map((s, i) => (s.seq === i ? s : { ...s, seq: i }));
598
+ }
599
+ /**
600
+ * Compatibility shim for snapshot-from-events use cases (replay,
601
+ * post-hoc analysis, tests). For LIVE use, prefer attaching a
602
+ * `RunStepRecorder` directly via `runner.attach(rec)` —
603
+ * `buildRunSteps(events)` constructs a fresh recorder, replays the
604
+ * events through its handlers, and returns the resulting entries.
605
+ *
606
+ * @deprecated Prefer `runStepRecorder()` + `runner.attach(rec)` for
607
+ * live consumers. This shim remains for offline / testing
608
+ * scenarios where only a recorded event list is available.
609
+ */
610
+ function buildRunSteps(source, options = {}) {
611
+ const events = Array.isArray(source)
612
+ ? source
613
+ : source.getEvents();
614
+ const rec = new RunStepRecorder();
615
+ // Coalesce fork.branch bursts before replay. BoundaryRecorder emits
616
+ // N fork.branch events per fork (all sharing parent+ts); the
617
+ // recorder expects ONE `onFork` call carrying all children. We pass
618
+ // through the first occurrence of each (parent, ts) pair as a single
619
+ // synthetic event with all children, and drop the rest.
620
+ const forkSeen = new Set();
621
+ for (let i = 0; i < events.length; i++) {
622
+ const e = events[i];
623
+ if (e.type === 'fork.branch') {
624
+ const key = `${e.parentSubflowId}@${e.ts}`;
625
+ if (forkSeen.has(key))
626
+ continue;
627
+ forkSeen.add(key);
628
+ const children = [];
629
+ for (let j = i; j < events.length; j++) {
630
+ const f = events[j];
631
+ if (f.type === 'fork.branch' && f.parentSubflowId === e.parentSubflowId && f.ts === e.ts) {
632
+ children.push(f.childName);
633
+ }
634
+ else if (f.ts > e.ts) {
635
+ break;
636
+ }
637
+ }
638
+ rec.onFork({
639
+ parent: e.parentSubflowId,
640
+ children,
641
+ traversalContext: {
642
+ runId: 'replay',
643
+ stageId: lastSegment(e.subflowPath.join('/') || trace_1.ROOT_SUBFLOW_ID),
644
+ runtimeStageId: e.runtimeStageId,
645
+ stageName: lastSegment(e.subflowPath.join('/') || trace_1.ROOT_SUBFLOW_ID),
646
+ depth: e.depth,
647
+ },
648
+ });
649
+ continue;
650
+ }
651
+ rec.ingestDomainEvent(e);
652
+ }
653
+ return [...rec.getSteps(options.drillPath)];
654
+ }
655
+ exports.buildRunSteps = buildRunSteps;
656
+ // Touch unused imports defensively for tree-shaking.
657
+ void trace_1.ROOT_RUNTIME_STAGE_ID;
658
+ //# sourceMappingURL=RunStepRecorder.js.map