footprintjs 4.17.2 → 6.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 (167) hide show
  1. package/CLAUDE.md +71 -9
  2. package/dist/advanced.js +2 -3
  3. package/dist/esm/advanced.js +2 -2
  4. package/dist/esm/index.js +3 -3
  5. package/dist/esm/lib/builder/FlowChartBuilder.js +359 -70
  6. package/dist/esm/lib/builder/index.js +1 -1
  7. package/dist/esm/lib/builder/structure/StructureRecorder.js +158 -0
  8. package/dist/esm/lib/builder/structure/StructureRecorderDispatcher.js +171 -0
  9. package/dist/esm/lib/builder/types.js +1 -1
  10. package/dist/esm/lib/decide/decide.js +9 -9
  11. package/dist/esm/lib/decide/evidence.js +2 -2
  12. package/dist/esm/lib/engine/errors/errorInfo.js +4 -3
  13. package/dist/esm/lib/engine/handlers/ChildrenExecutor.js +11 -2
  14. package/dist/esm/lib/engine/handlers/DeciderHandler.js +5 -7
  15. package/dist/esm/lib/engine/handlers/RuntimeStructureManager.js +2 -2
  16. package/dist/esm/lib/engine/handlers/SelectorHandler.js +9 -7
  17. package/dist/esm/lib/engine/handlers/SubflowExecutor.js +7 -1
  18. package/dist/esm/lib/engine/handlers/index.js +1 -3
  19. package/dist/esm/lib/engine/handlers/types.js +2 -2
  20. package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +14 -4
  21. package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +5 -5
  22. package/dist/esm/lib/engine/narrative/NarrativeFlowRecorder.js +8 -1
  23. package/dist/esm/lib/engine/narrative/types.js +1 -1
  24. package/dist/esm/lib/engine/runtimeStageId.js +64 -2
  25. package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +30 -56
  26. package/dist/esm/lib/engine/types.js +1 -1
  27. package/dist/esm/lib/engine/walkSubflowSpec.js +144 -0
  28. package/dist/esm/lib/memory/StageContext.js +3 -3
  29. package/dist/esm/lib/memory/backtrack.js +1 -1
  30. package/dist/esm/lib/reactive/createTypedScope.js +5 -5
  31. package/dist/esm/lib/reactive/types.js +7 -7
  32. package/dist/esm/lib/recorder/BoundaryStateStore.js +167 -0
  33. package/dist/esm/lib/recorder/BoundaryStateTracker.js +4 -4
  34. package/dist/esm/lib/recorder/CombinedRecorder.js +9 -9
  35. package/dist/esm/lib/recorder/CommitRangeIndex.js +207 -0
  36. package/dist/esm/lib/recorder/CompositeRecorder.js +6 -6
  37. package/dist/esm/lib/recorder/EmitRecorder.js +3 -3
  38. package/dist/esm/lib/recorder/InOutRecorder.js +1 -1
  39. package/dist/esm/lib/recorder/KeyedStore.js +113 -0
  40. package/dist/esm/lib/recorder/QualityRecorder.js +2 -2
  41. package/dist/esm/lib/recorder/SequenceRecorder.js +2 -2
  42. package/dist/esm/lib/recorder/SequenceStore.js +195 -0
  43. package/dist/esm/lib/recorder/TopologyRecorder.js +1 -1
  44. package/dist/esm/lib/recorder/index.js +13 -3
  45. package/dist/esm/lib/runner/ExecutionRuntime.js +1 -1
  46. package/dist/esm/lib/runner/FlowChartExecutor.js +75 -44
  47. package/dist/esm/lib/runner/RunContext.js +2 -2
  48. package/dist/esm/lib/runner/RunnableChart.js +1 -1
  49. package/dist/esm/lib/runner/getSubtreeSnapshot.js +3 -2
  50. package/dist/esm/lib/runner/runId.js +65 -0
  51. package/dist/esm/lib/scope/ScopeFacade.js +6 -6
  52. package/dist/esm/lib/scope/index.js +1 -1
  53. package/dist/esm/lib/scope/recorders/DebugRecorder.js +4 -4
  54. package/dist/esm/lib/scope/recorders/MetricRecorder.js +2 -2
  55. package/dist/esm/lib/scope/recorders/index.js +1 -1
  56. package/dist/esm/lib/scope/types.js +1 -1
  57. package/dist/esm/recorders.js +1 -1
  58. package/dist/esm/trace.js +15 -2
  59. package/dist/index.js +3 -3
  60. package/dist/lib/builder/FlowChartBuilder.js +359 -70
  61. package/dist/lib/builder/index.js +1 -1
  62. package/dist/lib/builder/structure/StructureRecorder.js +159 -0
  63. package/dist/lib/builder/structure/StructureRecorderDispatcher.js +175 -0
  64. package/dist/lib/builder/types.js +1 -1
  65. package/dist/lib/decide/decide.js +9 -9
  66. package/dist/lib/decide/evidence.js +2 -2
  67. package/dist/lib/engine/errors/errorInfo.js +4 -3
  68. package/dist/lib/engine/handlers/ChildrenExecutor.js +11 -2
  69. package/dist/lib/engine/handlers/DeciderHandler.js +5 -7
  70. package/dist/lib/engine/handlers/RuntimeStructureManager.js +2 -2
  71. package/dist/lib/engine/handlers/SelectorHandler.js +9 -7
  72. package/dist/lib/engine/handlers/SubflowExecutor.js +7 -1
  73. package/dist/lib/engine/handlers/index.js +2 -5
  74. package/dist/lib/engine/handlers/types.js +2 -2
  75. package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +14 -4
  76. package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +5 -5
  77. package/dist/lib/engine/narrative/NarrativeFlowRecorder.js +8 -1
  78. package/dist/lib/engine/narrative/types.js +1 -1
  79. package/dist/lib/engine/runtimeStageId.js +66 -3
  80. package/dist/lib/engine/traversal/FlowchartTraverser.js +30 -56
  81. package/dist/lib/engine/types.js +1 -1
  82. package/dist/lib/engine/walkSubflowSpec.js +148 -0
  83. package/dist/lib/memory/StageContext.js +3 -3
  84. package/dist/lib/memory/backtrack.js +1 -1
  85. package/dist/lib/reactive/createTypedScope.js +5 -5
  86. package/dist/lib/reactive/types.js +7 -7
  87. package/dist/lib/recorder/BoundaryStateStore.js +171 -0
  88. package/dist/lib/recorder/BoundaryStateTracker.js +4 -4
  89. package/dist/lib/recorder/CombinedRecorder.js +9 -9
  90. package/dist/lib/recorder/CommitRangeIndex.js +211 -0
  91. package/dist/lib/recorder/CompositeRecorder.js +6 -6
  92. package/dist/lib/recorder/EmitRecorder.js +3 -3
  93. package/dist/lib/recorder/InOutRecorder.js +1 -1
  94. package/dist/lib/recorder/KeyedStore.js +117 -0
  95. package/dist/lib/recorder/QualityRecorder.js +2 -2
  96. package/dist/lib/recorder/SequenceRecorder.js +2 -2
  97. package/dist/lib/recorder/SequenceStore.js +199 -0
  98. package/dist/lib/recorder/TopologyRecorder.js +1 -1
  99. package/dist/lib/recorder/index.js +19 -6
  100. package/dist/lib/runner/ExecutionRuntime.js +1 -1
  101. package/dist/lib/runner/FlowChartExecutor.js +75 -44
  102. package/dist/lib/runner/RunContext.js +2 -2
  103. package/dist/lib/runner/RunnableChart.js +1 -1
  104. package/dist/lib/runner/getSubtreeSnapshot.js +3 -2
  105. package/dist/lib/runner/runId.js +70 -0
  106. package/dist/lib/scope/ScopeFacade.js +6 -6
  107. package/dist/lib/scope/index.js +1 -1
  108. package/dist/lib/scope/recorders/DebugRecorder.js +4 -4
  109. package/dist/lib/scope/recorders/MetricRecorder.js +2 -2
  110. package/dist/lib/scope/recorders/index.js +1 -1
  111. package/dist/lib/scope/types.js +1 -1
  112. package/dist/recorders.js +1 -1
  113. package/dist/trace.js +21 -2
  114. package/dist/types/advanced.d.ts +4 -4
  115. package/dist/types/index.d.ts +6 -4
  116. package/dist/types/lib/builder/FlowChartBuilder.d.ts +110 -14
  117. package/dist/types/lib/builder/index.d.ts +2 -1
  118. package/dist/types/lib/builder/structure/StructureRecorder.d.ts +349 -0
  119. package/dist/types/lib/builder/structure/StructureRecorderDispatcher.d.ts +77 -0
  120. package/dist/types/lib/builder/types.d.ts +20 -6
  121. package/dist/types/lib/decide/evidence.d.ts +3 -3
  122. package/dist/types/lib/engine/errors/errorInfo.d.ts +3 -2
  123. package/dist/types/lib/engine/handlers/DeciderHandler.d.ts +3 -3
  124. package/dist/types/lib/engine/handlers/RuntimeStructureManager.d.ts +1 -1
  125. package/dist/types/lib/engine/handlers/SelectorHandler.d.ts +2 -2
  126. package/dist/types/lib/engine/handlers/index.d.ts +1 -2
  127. package/dist/types/lib/engine/handlers/types.d.ts +2 -9
  128. package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +1 -1
  129. package/dist/types/lib/engine/narrative/FlowRecorderDispatcher.d.ts +4 -4
  130. package/dist/types/lib/engine/narrative/types.d.ts +43 -4
  131. package/dist/types/lib/engine/runtimeStageId.d.ts +57 -1
  132. package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +12 -6
  133. package/dist/types/lib/engine/types.d.ts +0 -26
  134. package/dist/types/lib/engine/walkSubflowSpec.d.ts +95 -0
  135. package/dist/types/lib/memory/StageContext.d.ts +2 -2
  136. package/dist/types/lib/memory/backtrack.d.ts +1 -1
  137. package/dist/types/lib/reactive/types.d.ts +7 -7
  138. package/dist/types/lib/recorder/BoundaryStateStore.d.ts +115 -0
  139. package/dist/types/lib/recorder/BoundaryStateTracker.d.ts +3 -3
  140. package/dist/types/lib/recorder/CombinedRecorder.d.ts +11 -11
  141. package/dist/types/lib/recorder/CommitRangeIndex.d.ts +147 -0
  142. package/dist/types/lib/recorder/CompositeRecorder.d.ts +8 -8
  143. package/dist/types/lib/recorder/EmitRecorder.d.ts +2 -2
  144. package/dist/types/lib/recorder/InOutRecorder.d.ts +1 -1
  145. package/dist/types/lib/recorder/KeyedStore.d.ts +70 -0
  146. package/dist/types/lib/recorder/QualityRecorder.d.ts +4 -4
  147. package/dist/types/lib/recorder/SequenceRecorder.d.ts +1 -1
  148. package/dist/types/lib/recorder/SequenceStore.d.ts +120 -0
  149. package/dist/types/lib/recorder/TopologyRecorder.d.ts +1 -1
  150. package/dist/types/lib/recorder/index.d.ts +5 -2
  151. package/dist/types/lib/runner/ExecutionRuntime.d.ts +1 -1
  152. package/dist/types/lib/runner/FlowChartExecutor.d.ts +40 -30
  153. package/dist/types/lib/runner/RunContext.d.ts +2 -2
  154. package/dist/types/lib/runner/RunnableChart.d.ts +2 -2
  155. package/dist/types/lib/runner/runId.d.ts +45 -0
  156. package/dist/types/lib/scope/ScopeFacade.d.ts +4 -4
  157. package/dist/types/lib/scope/index.d.ts +1 -1
  158. package/dist/types/lib/scope/recorders/DebugRecorder.d.ts +4 -4
  159. package/dist/types/lib/scope/recorders/MetricRecorder.d.ts +4 -4
  160. package/dist/types/lib/scope/recorders/index.d.ts +1 -1
  161. package/dist/types/lib/scope/types.d.ts +1 -1
  162. package/dist/types/recorders.d.ts +1 -1
  163. package/dist/types/trace.d.ts +8 -1
  164. package/package.json +1 -1
  165. package/dist/esm/lib/engine/handlers/ExtractorRunner.js +0 -122
  166. package/dist/lib/engine/handlers/ExtractorRunner.js +0 -126
  167. package/dist/types/lib/engine/handlers/ExtractorRunner.d.ts +0 -41
@@ -13,9 +13,25 @@ import type { StructuredErrorInfo } from '../errors/errorInfo.js';
13
13
  * Uses Null Object pattern: NullControlFlowNarrativeGenerator satisfies this
14
14
  * interface with empty methods for zero-cost disabled path.
15
15
  */
16
+ /**
17
+ * The kind of stage that completed. Lets consumers route uniform
18
+ * "did this stage execute" handling without a side-table lookup into
19
+ * the chart spec. Required on every `onStageExecuted` event since
20
+ * proposal #003 unified the event to fire for ALL stage kinds.
21
+ */
22
+ export type StageType = 'linear' | 'decider' | 'fork' | 'selector' | 'subflow-mount';
16
23
  export interface IControlFlowNarrative {
17
- /** Called when a stage executes. First stage uses distinct opening pattern. */
18
- onStageExecuted(stageName: string, description?: string, traversalContext?: TraversalContext): void;
24
+ /**
25
+ * Called when a stage completes its main work. Fires for ALL stage
26
+ * kinds (`'linear'`, `'decider'`, `'fork'`, `'selector'`, `'subflow-mount'`)
27
+ * AFTER the corresponding specialized event (`onDecision`, `onFork`,
28
+ * `onSelected`, `onSubflowEntry`). For linear stages, fires after
29
+ * the stage function returns.
30
+ *
31
+ * Consumers tracking "did this stage run?" use this event uniformly
32
+ * and switch on `stageType` for kind-specific work.
33
+ */
34
+ onStageExecuted(stageName: string, description: string | undefined, traversalContext: TraversalContext | undefined, stageType: StageType): void;
19
35
  /** Called on linear continuation from one stage to the next. */
20
36
  onNext(fromStage: string, toStage: string, description?: string, traversalContext?: TraversalContext): void;
21
37
  /** Called when a decider selects a branch. Most valuable for LLM context. */
@@ -73,9 +89,26 @@ export interface IControlFlowNarrative {
73
89
  * Like OpenTelemetry's span context: stageId + parentStageId form a tree.
74
90
  */
75
91
  export interface TraversalContext {
92
+ /**
93
+ * Per-`executor.run()` identifier. Generated once at the start of every
94
+ * `run()` (and again on `resume()`); shared by every event of that run;
95
+ * differs across consecutive runs of the same executor.
96
+ *
97
+ * Format: `${Date.now()}-${counter}` — sortable lexicographically (==
98
+ * chronologically for runs > 1ms apart). Process-local — for cross-
99
+ * process correlation use `getEnv().traceId` (consumer-supplied).
100
+ *
101
+ * Recorders that accumulate state across runs (fork bookkeeping,
102
+ * sibling-handoff state, etc.) detect "new run" via
103
+ * `event.traversalContext.runId !== this.lastRunId` and reset
104
+ * transient bookkeeping. Recorders that don't care about scoping
105
+ * ignore the field.
106
+ */
107
+ readonly runId: string;
76
108
  /** Stable stage identifier from the builder (matches spec node id). */
77
109
  readonly stageId: string;
78
- /** Unique per-execution-step identifier. Format: [subflowPath/]stageId#executionIndex */
110
+ /** Unique per-execution-step identifier. Format: [subflowPath/]stageId#executionIndex.
111
+ * Counter resets per executor — combine with `runId` for globally unique step keys. */
79
112
  readonly runtimeStageId: string;
80
113
  /** Human-readable stage name. */
81
114
  readonly stageName: string;
@@ -98,6 +131,12 @@ export interface FlowStageEvent {
98
131
  description?: string;
99
132
  /** Traversal context from the engine — read-only, set by traverser. */
100
133
  traversalContext?: TraversalContext;
134
+ /**
135
+ * Which kind of stage completed. The engine fires `onStageExecuted`
136
+ * uniformly for every stage kind (proposal #003); consumers route by
137
+ * `stageType` without a chart-spec lookup.
138
+ */
139
+ stageType: StageType;
101
140
  }
102
141
  /** Event passed to FlowRecorder.onNext. */
103
142
  export interface FlowNextEvent {
@@ -228,7 +267,7 @@ export interface FlowRunEvent {
228
267
  /**
229
268
  * FlowRecorder — Pluggable observer for control flow events.
230
269
  *
231
- * Mirrors the scope-level Recorder pattern for the engine layer.
270
+ * Mirrors the scope-level ScopeRecorder pattern for the engine layer.
232
271
  * All methods are optional — implement only the hooks you need.
233
272
  * Recorders are invoked synchronously in attachment order.
234
273
  * If a recorder throws, the error is caught and swallowed; execution continues.
@@ -14,6 +14,15 @@
14
14
  * - Human-readable ('sf-tools/execute-tool-calls#8')
15
15
  * - Parseable (split on '#' for stageId and index, split stageId on '/' for subflow path)
16
16
  *
17
+ * Naming-collision warning
18
+ * ────────────────────────
19
+ * The parsed-output `.stageId` field below is the LOCAL form (segment
20
+ * after the last '/'). This is NOT the same as `spec.id` / `node.id`
21
+ * for subflow-nested stages, which carry the FULL prefixed form
22
+ * (`'sf-tools/execute-tool-calls'`). To compare safely, use
23
+ * `splitStageId(spec.id)` to decompose the prefixed form the same
24
+ * way `parseRuntimeStageId` decomposes a runtimeStageId.
25
+ *
17
26
  * @example
18
27
  * ```
19
28
  * buildRuntimeStageId('call-llm', 5) // 'call-llm#5'
@@ -29,12 +38,59 @@
29
38
  * consumers constructing IDs from parsed components (round-trip via parseRuntimeStageId).
30
39
  */
31
40
  export declare function buildRuntimeStageId(stageId: string, executionIndex: number, subflowPath?: string): string;
32
- /** Parse a runtimeStageId into its components. */
41
+ /**
42
+ * Parse a runtimeStageId into its components.
43
+ *
44
+ * IMPORTANT — naming collision: the returned `stageId` is the LOCAL
45
+ * form (the segment between the last '/' and the '#'). This is NOT
46
+ * the same as `spec.id` or `node.id` for subflow-nested stages,
47
+ * which contain the FULL prefixed form.
48
+ *
49
+ * parseRuntimeStageId('sf-tools/execute-tool-calls#8').stageId
50
+ * // → 'execute-tool-calls' (LOCAL)
51
+ *
52
+ * node.id // (post-mount, in a spec that contains subflows)
53
+ * // → 'sf-tools/execute-tool-calls' (FULL prefixed)
54
+ *
55
+ * To compare these two safely, use `splitStageId(node.id)` to get
56
+ * the local form, OR reconstruct the full form via
57
+ * `(subflowPath ? subflowPath + '/' : '') + stageId`.
58
+ */
33
59
  export declare function parseRuntimeStageId(runtimeStageId: string): {
34
60
  stageId: string;
35
61
  executionIndex: number;
36
62
  subflowPath: string | undefined;
37
63
  };
64
+ /**
65
+ * Decompose a (possibly prefixed) stage id into its components.
66
+ *
67
+ * Use this when you have an id WITHOUT the `#N` execution suffix and
68
+ * need the local stage name and/or the subflow path. Common sources
69
+ * of such ids:
70
+ * - `spec.id` (post-mount the id includes any subflow prefix)
71
+ * - `CommitBundle.stageId` (post-mount id)
72
+ * - `node.id` from xyflow nodes built off the spec
73
+ * - the segment of `runtimeStageId` BEFORE the `#` (use
74
+ * `parseRuntimeStageId` directly for full runtimeStageId strings)
75
+ *
76
+ * Mirrors the decomposition `parseRuntimeStageId` performs on the
77
+ * stageId portion of a runtimeStageId, so the two helpers stay in
78
+ * lockstep on naming and behavior.
79
+ *
80
+ * @example
81
+ * splitStageId('sf-tools/execute-tool-calls')
82
+ * // → { localStageId: 'execute-tool-calls', subflowPath: 'sf-tools' }
83
+ *
84
+ * splitStageId('execute-tool-calls')
85
+ * // → { localStageId: 'execute-tool-calls', subflowPath: undefined }
86
+ *
87
+ * splitStageId('sf-outer/sf-inner/validate')
88
+ * // → { localStageId: 'validate', subflowPath: 'sf-outer/sf-inner' }
89
+ */
90
+ export declare function splitStageId(prefixedStageId: string): {
91
+ localStageId: string;
92
+ subflowPath: string | undefined;
93
+ };
38
94
  /**
39
95
  * Shared mutable counter for execution index.
40
96
  * Passed by reference to child traversers (subflows) so they
@@ -21,7 +21,7 @@
21
21
  import type { ScopeProtectionMode } from '../../scope/protection/types.js';
22
22
  import { FlowRecorderDispatcher } from '../narrative/FlowRecorderDispatcher.js';
23
23
  import type { FlowRecorder, IControlFlowNarrative } from '../narrative/types.js';
24
- import type { ExtractorError, IExecutionRuntime, ILogger, ScopeFactory, SerializedPipelineStructure, StageFunction, StageNode, StreamHandlers, SubflowResult, TraversalExtractor, TraversalResult } from '../types.js';
24
+ import type { IExecutionRuntime, ILogger, ScopeFactory, SerializedPipelineStructure, StageFunction, StageNode, StreamHandlers, SubflowResult, TraversalResult } from '../types.js';
25
25
  export interface TraverserOptions<TOut = any, TScope = any> {
26
26
  root: StageNode<TOut, TScope>;
27
27
  stageMap: Map<string, StageFunction<TOut, TScope>>;
@@ -32,12 +32,10 @@ export interface TraverserOptions<TOut = any, TScope = any> {
32
32
  executionEnv?: import('../../engine/types').ExecutionEnv;
33
33
  throttlingErrorChecker?: (error: unknown) => boolean;
34
34
  streamHandlers?: StreamHandlers;
35
- extractor?: TraversalExtractor;
36
35
  scopeProtectionMode?: ScopeProtectionMode;
37
36
  subflows?: Record<string, {
38
37
  root: StageNode<TOut, TScope>;
39
38
  }>;
40
- enrichSnapshots?: boolean;
41
39
  narrativeEnabled?: boolean;
42
40
  buildTimeStructure?: SerializedPipelineStructure;
43
41
  logger: ILogger;
@@ -71,6 +69,14 @@ export interface TraverserOptions<TOut = any, TScope = any> {
71
69
  * the inputMapper. Undefined on normal `run()` paths.
72
70
  */
73
71
  subflowStatesForResume?: Record<string, Record<string, unknown>>;
72
+ /**
73
+ * Per-`executor.run()` identifier. Threaded into every TraversalContext
74
+ * this traverser produces so recorders can scope state to a single run.
75
+ * Subflow traversers inherit the parent's runId (the subflow is part of
76
+ * the same run from the consumer's POV). Required field — every event
77
+ * needs it. See `runner/runId.ts`.
78
+ */
79
+ runId: string;
74
80
  }
75
81
  export declare class FlowchartTraverser<TOut = any, TScope = any> {
76
82
  private readonly root;
@@ -84,6 +90,9 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
84
90
  * top-level traversal so consumers (e.g. `InOutRecorder`) can bracket
85
91
  * the run with the same payload shape that subflows already have. */
86
92
  private readonly readOnlyContext?;
93
+ /** Per-`executor.run()` identifier. Stamped onto every TraversalContext.
94
+ * Inherited by subflow traversers so all events of one run share one runId. */
95
+ private readonly runId;
87
96
  private readonly nodeResolver;
88
97
  private readonly childrenExecutor;
89
98
  private readonly subflowExecutor;
@@ -92,7 +101,6 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
92
101
  private readonly deciderHandler;
93
102
  private readonly selectorHandler;
94
103
  private readonly structureManager;
95
- private readonly extractorRunner;
96
104
  private readonly narrativeGenerator;
97
105
  private readonly flowRecorderDispatcher;
98
106
  private subflowResults;
@@ -183,8 +191,6 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
183
191
  getBranchIds(): string[];
184
192
  getRuntimeRoot(): StageNode;
185
193
  getSubflowResults(): Map<string, SubflowResult>;
186
- getExtractedResults<TResult = unknown>(): Map<string, TResult>;
187
- getExtractorErrors(): ExtractorError[];
188
194
  getNarrative(): string[];
189
195
  /** Returns the FlowRecorderDispatcher, or undefined if narrative is disabled. */
190
196
  getFlowRecorderDispatcher(): FlowRecorderDispatcher | undefined;
@@ -323,32 +323,6 @@ export interface RuntimeStructureMetadata {
323
323
  isLoopReference?: boolean;
324
324
  streamId?: string;
325
325
  }
326
- export interface StageSnapshot<TOut = any, TScope = any> {
327
- node: StageNode<TOut, TScope>;
328
- context: StageContext;
329
- stepNumber: number;
330
- structureMetadata: RuntimeStructureMetadata;
331
- scopeState?: Record<string, unknown>;
332
- debugInfo?: {
333
- logs: Record<string, unknown>;
334
- errors: Record<string, unknown>;
335
- metrics: Record<string, unknown>;
336
- evals: Record<string, unknown>;
337
- flowMessages?: FlowMessage[];
338
- };
339
- stageOutput?: unknown;
340
- errorInfo?: {
341
- type: string;
342
- message: string;
343
- };
344
- historyIndex?: number;
345
- }
346
- export type TraversalExtractor<TResult = unknown> = (snapshot: StageSnapshot) => TResult | undefined | null;
347
- export interface ExtractorError {
348
- stagePath: string;
349
- message: string;
350
- error: unknown;
351
- }
352
326
  export type NodeResultType = {
353
327
  id: string;
354
328
  result: unknown;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * walkSubflowSpec — yield the structural shape of a subflow spec as a
3
+ * flat ordered stream of items, with `subflowPath` already composed
4
+ * for nested subflows.
5
+ *
6
+ * This is the public contract for traversing the structure delivered
7
+ * via `StructureSubflowMountedEvent.subflowSpec`. Item shapes mirror
8
+ * the corresponding Structure event payloads so consumers can route
9
+ * walker items through the same handlers they use for live events.
10
+ *
11
+ * Walker contract (LOCKED):
12
+ * 1. AUTO-RECURSE by default into nested subflows, with composed
13
+ * paths (`parent/child/...`). Pass `{ recurse: false }` to walk
14
+ * only one level.
15
+ * 2. ENTRY-STAGE MARKER FIRST: for each subflow (top-level and
16
+ * nested), yields a `{ kind: 'subflow-start', ... }` item BEFORE
17
+ * any stage/edge items from that subflow. Lets consumers draw the
18
+ * boundary edge from the mount node to the entry stage.
19
+ * 3. COMPOSED PATHS: nested subflows get `parentPath + '/' + localId`.
20
+ * Top-level mount paths are local-only (`'auth'`, NOT `'__root__/auth'`).
21
+ * 4. SHAPE MIRRORING: stage/edge/loop items have the same payload
22
+ * shape as Structure events, with `subflowPath` added.
23
+ * 5. SOURCE DISCRIMINATOR: every walker item carries `source: 'walker'`
24
+ * (Structure events do NOT). Lets consumers distinguish event vs
25
+ * walker in logs/debuggers while still sharing handler code paths.
26
+ * 6. STAGE-ID PREFIXING: stage IDs in nested subflows are already
27
+ * prefixed by the spec (e.g. `'auth/verify/check'`). Walker
28
+ * preserves this; `subflowPath` field is redundant-but-explicit.
29
+ */
30
+ import type { SerializedPipelineStructure } from '../builder/types.js';
31
+ export interface WalkerOptions {
32
+ /** Auto-recurse into nested subflows (default: true). When false,
33
+ * nested subflow items are yielded but their internals are not
34
+ * traversed. */
35
+ recurse?: boolean;
36
+ }
37
+ export type WalkerItem = {
38
+ kind: 'subflow-start';
39
+ stageId: string;
40
+ subflowPath: string;
41
+ source: 'walker';
42
+ } | {
43
+ kind: 'stage';
44
+ stageId: string;
45
+ name: string;
46
+ type: NonNullable<SerializedPipelineStructure['type']>;
47
+ isPausable?: boolean;
48
+ spec: SerializedPipelineStructure;
49
+ subflowPath: string;
50
+ source: 'walker';
51
+ } | {
52
+ kind: 'edge';
53
+ from: string;
54
+ to: string;
55
+ edgeKind: 'next' | 'fork-branch' | 'decision-branch';
56
+ label?: string;
57
+ subflowPath: string;
58
+ source: 'walker';
59
+ } | {
60
+ kind: 'loop';
61
+ from: string;
62
+ to: string;
63
+ subflowPath: string;
64
+ source: 'walker';
65
+ } | {
66
+ kind: 'subflow';
67
+ mountStageId: string;
68
+ subflowId: string;
69
+ subflowName: string;
70
+ subflowSpec: SerializedPipelineStructure;
71
+ subflowPath: string;
72
+ source: 'walker';
73
+ };
74
+ /**
75
+ * Walk a subflow spec, yielding its structure as flat ordered items.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { walkSubflowSpec } from 'footprintjs/trace';
80
+ *
81
+ * onSubflowMounted(event) {
82
+ * if (!event.subflowSpec) return; // lazy mount — no spec yet
83
+ * for (const item of walkSubflowSpec(event.subflowSpec, event.subflowPath)) {
84
+ * switch (item.kind) {
85
+ * case 'subflow-start': break; // entry boundary
86
+ * case 'stage': break; // inner stage
87
+ * case 'edge': break; // inner edge
88
+ * case 'loop': break; // inner loop back-edge
89
+ * case 'subflow': break; // nested mount marker
90
+ * }
91
+ * }
92
+ * }
93
+ * ```
94
+ */
95
+ export declare function walkSubflowSpec(spec: SerializedPipelineStructure, subflowPath: string, options?: WalkerOptions): Generator<WalkerItem, void, void>;
@@ -50,7 +50,7 @@ export declare class StageContext {
50
50
  private _stageWrites;
51
51
  /** Tracks user-level reads (pre-namespace) for the memory view. */
52
52
  private _stageReads;
53
- /** Observer called after commit() — used by ScopeFacade to fire Recorder.onCommit. */
53
+ /** Observer called after commit() — used by ScopeFacade to fire ScopeRecorder.onCommit. */
54
54
  private _commitObserver?;
55
55
  constructor(runId: string, name: string, stageId: string, sharedMemory: SharedMemory, branchId?: string, eventLog?: EventLog, isDecider?: boolean);
56
56
  /** Returns the SharedMemory instance (needed by scope layer). */
@@ -89,7 +89,7 @@ export declare class StageContext {
89
89
  getScope(): Record<string, unknown>;
90
90
  getRunId(): string;
91
91
  /** Register an observer that fires after commit() applies patches.
92
- * Used by ScopeFacade to dispatch Recorder.onCommit events. */
92
+ * Used by ScopeFacade to dispatch ScopeRecorder.onCommit events. */
93
93
  setCommitObserver(observer: (mutations: Record<string, {
94
94
  value: unknown;
95
95
  operation: 'set' | 'update' | 'delete';
@@ -84,7 +84,7 @@ export interface CausalChainOptions {
84
84
  * which read→write edges to follow.
85
85
  *
86
86
  * Implementors: QualityRecorder tracks keysRead per step,
87
- * or build a Map<runtimeStageId, string[]> from Recorder.onRead events.
87
+ * or build a Map<runtimeStageId, string[]> from ScopeRecorder.onRead events.
88
88
  */
89
89
  export type KeysReadLookup = (runtimeStageId: string) => string[];
90
90
  /**
@@ -8,7 +8,7 @@
8
8
  * Dependency: type-only imports from engine/ and scope/ (zero runtime cost).
9
9
  */
10
10
  import type { ExecutionEnv } from '../engine/types.js';
11
- import type { Recorder } from '../scope/types.js';
11
+ import type { ScopeRecorder } from '../scope/types.js';
12
12
  export interface ReactiveTarget {
13
13
  getValue(key?: string): unknown;
14
14
  setValue(key: string, value: unknown, shouldRedact?: boolean, description?: string): void;
@@ -22,9 +22,9 @@ export interface ReactiveTarget {
22
22
  getValueSilent?(key?: string): unknown;
23
23
  getArgs<T = Record<string, unknown>>(): T;
24
24
  getEnv(): Readonly<ExecutionEnv>;
25
- attachRecorder(recorder: Recorder): void;
26
- detachRecorder(recorderId: string): void;
27
- getRecorders(): Recorder[];
25
+ attachScopeRecorder(recorder: ScopeRecorder): void;
26
+ detachScopeRecorder(recorderId: string): void;
27
+ getScopeRecorders(): ScopeRecorder[];
28
28
  addDebugInfo(key: string, value: unknown): void;
29
29
  addDebugMessage(value: unknown): void;
30
30
  addErrorInfo(key: string, value: unknown): void;
@@ -48,9 +48,9 @@ export interface ScopeMethods {
48
48
  $error(key: string, value: unknown): void;
49
49
  $metric(name: string, value: unknown): void;
50
50
  $eval(name: string, value: unknown): void;
51
- $attachRecorder(recorder: Recorder): void;
52
- $detachRecorder(recorderId: string): void;
53
- $getRecorders(): Recorder[];
51
+ $attachScopeRecorder(recorder: ScopeRecorder): void;
52
+ $detachScopeRecorder(recorderId: string): void;
53
+ $getScopeRecorders(): ScopeRecorder[];
54
54
  /**
55
55
  * Batch-mutate an array key in a single clone+write cycle.
56
56
  *
@@ -0,0 +1,115 @@
1
+ /**
2
+ * BoundaryStateStore<TState> — concrete, composable per-boundary
3
+ * transient state storage.
4
+ *
5
+ * Pattern: COMPOSITION primitive. Concrete class — instantiate with
6
+ * `new BoundaryStateStore<TState>()` and own it as a field
7
+ * on your recorder. Replaces the abstract
8
+ * `BoundaryStateTracker<TState>` base class for the v5
9
+ * "one purpose per recorder" rule.
10
+ * Role: "What's the LIVE transient state of every currently-active
11
+ * boundary?" A boundary is a matched event pair `[start, stop]`
12
+ * bracketing an interval (e.g., `(llm_start, llm_end)`).
13
+ * Between the brackets, intermediate events evolve the
14
+ * boundary's state. On `stop`, the state clears.
15
+ *
16
+ * Algorithmically this is the **DFS bracket-sequence pattern** — the
17
+ * `active` map is the open-brackets stack at any moment.
18
+ *
19
+ * **Lifecycle contract — STRICT:** every `start(key, ...)` call MUST
20
+ * be paired with a `stop(key)` call. Failure to wire stop produces a
21
+ * memory leak: the active map grows without bound.
22
+ *
23
+ * **Concurrency / nesting:** concurrent boundaries (parallel branches
24
+ * with two LLM calls active at once) work correctly — each is keyed
25
+ * independently. Nested boundaries of DIFFERENT KINDS require
26
+ * SEPARATE store instances — one per kind.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { BoundaryStateStore } from 'footprintjs/trace';
31
+ * import type { CombinedRecorder, EmitEvent } from 'footprintjs';
32
+ *
33
+ * interface LLMLiveState { partial: string; tokens: number; }
34
+ *
35
+ * class LiveLLMTracker implements CombinedRecorder {
36
+ * readonly id = 'live-llm';
37
+ * private readonly store = new BoundaryStateStore<LLMLiveState>();
38
+ *
39
+ * onEmit(event: EmitEvent): void {
40
+ * if (event.name === 'agentfootprint.stream.llm_start') {
41
+ * this.store.start(event.runtimeStageId, { partial: '', tokens: 0 });
42
+ * } else if (event.name === 'agentfootprint.stream.llm_end') {
43
+ * this.store.stop(event.runtimeStageId);
44
+ * } else if (event.name === 'agentfootprint.stream.token') {
45
+ * this.store.update(event.runtimeStageId, (s) => ({
46
+ * partial: s.partial + (event.payload as { content: string }).content,
47
+ * tokens: s.tokens + 1,
48
+ * }));
49
+ * }
50
+ * }
51
+ *
52
+ * isInFlight(): boolean { return this.store.hasActive; }
53
+ * getPartial(rid: string): string {
54
+ * return this.store.get(rid)?.partial ?? '';
55
+ * }
56
+ *
57
+ * clear() { this.store.clear(); }
58
+ * }
59
+ * ```
60
+ */
61
+ export declare class BoundaryStateStore<TState> {
62
+ /** Open-brackets stack: key → current transient state. */
63
+ private readonly active;
64
+ /** Per-key count of `update` calls that landed without a matching
65
+ * active boundary. Drives rate-limited dev-mode warnings so a
66
+ * stuck loop doesn't spam the console. */
67
+ private readonly missedUpdates;
68
+ /** Optional id for diagnostics — passed to dev-mode warnings so the
69
+ * source of the leak is easy to find when multiple stores coexist. */
70
+ private readonly diagnosticId;
71
+ constructor(diagnosticId?: string);
72
+ /**
73
+ * Open a new boundary with initial transient state. If a boundary
74
+ * with the same `key` is already active, the prior state is
75
+ * overwritten (last-writer-wins). Dev mode warns — usually
76
+ * indicates a missed `stop` upstream.
77
+ */
78
+ start(key: string, initial: TState): void;
79
+ /**
80
+ * Evolve the transient state of an active boundary using a pure
81
+ * updater function. Silent no-op if no boundary is active for `key`
82
+ * (defensive against out-of-order events). Dev mode logs a rate-
83
+ * limited warning.
84
+ */
85
+ update(key: string, updater: (prev: TState) => TState): void;
86
+ /**
87
+ * Close the boundary identified by `key` and return its FINAL
88
+ * transient state (so the consumer can do any cleanup — e.g., emit
89
+ * a snapshot to a SequenceStore for durable storage).
90
+ */
91
+ stop(key: string): TState | undefined;
92
+ /** Current transient state of ONE active boundary. `undefined` if no
93
+ * boundary is active for `key`. */
94
+ get(key: string): TState | undefined;
95
+ /** All currently-active boundaries.
96
+ *
97
+ * **Type-only readonly:** TypeScript prevents mutation through
98
+ * `ReadonlyMap`, but a runtime cast or non-TS consumer can mutate
99
+ * the underlying Map and corrupt state. Don't. */
100
+ getAll(): ReadonlyMap<string, TState>;
101
+ /** True if any boundary is currently active. O(1). */
102
+ get hasActive(): boolean;
103
+ /** Number of currently-active boundaries. O(1). */
104
+ get activeCount(): number;
105
+ /**
106
+ * Reset all transient state. Called by recorder composers from
107
+ * their own `clear()` method, which the executor invokes before
108
+ * each run.
109
+ *
110
+ * Dev mode warns if any boundaries are still active when called —
111
+ * likely indicates a missed `stop` upstream. Leaked keys are listed
112
+ * (truncated to 10) so the wiring bug is findable.
113
+ */
114
+ clear(): void;
115
+ }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * **Mental model — observers vs. bookkeepers:**
6
6
  *
7
- * `Recorder` / `FlowRecorder` / `EmitRecorder` / `CombinedRecorder`
7
+ * `ScopeRecorder` / `FlowRecorder` / `EmitRecorder` / `CombinedRecorder`
8
8
  * are OBSERVER interfaces — they describe how a recorder hears
9
9
  * events from the executor.
10
10
  *
@@ -57,8 +57,8 @@
57
57
  * - Aggregations across the whole run (totals, counts) — those are
58
58
  * `SequenceRecorder.aggregate()` / `KeyedRecorder.aggregate()`.
59
59
  *
60
- * - Stage-level concerns — those use `Recorder.onStageStart` /
61
- * `Recorder.onStageEnd`. This primitive operates at finer
60
+ * - Stage-level concerns — those use `ScopeRecorder.onStageStart` /
61
+ * `ScopeRecorder.onStageEnd`. This primitive operates at finer
62
62
  * granularity (events emitted DURING a stage execution).
63
63
  *
64
64
  * **Lifecycle contract — STRICT:**
@@ -7,9 +7,9 @@
7
7
  *
8
8
  * Before `CombinedRecorder`, a consumer who wanted to observe both streams
9
9
  * had to:
10
- * 1. Implement both `Recorder` (8 methods) and `FlowRecorder` (12 methods)
10
+ * 1. Implement both `ScopeRecorder` (8 methods) and `FlowRecorder` (12 methods)
11
11
  * fully — stubbing every event they didn't care about.
12
- * 2. Remember to call BOTH `attachRecorder(r)` AND `attachFlowRecorder(r)`.
12
+ * 2. Remember to call BOTH `attachScopeRecorder(r)` AND `attachFlowRecorder(r)`.
13
13
  * Forgetting the second call silently dropped half their events — no
14
14
  * warning, no runtime error.
15
15
  * 3. Re-implement coordination logic (buffering, ordering) themselves.
@@ -36,7 +36,7 @@
36
36
  *
37
37
  * const audit: CombinedRecorder = {
38
38
  * id: 'audit',
39
- * onWrite: (e) => logWrite(e.key, e.value), // Recorder method
39
+ * onWrite: (e) => logWrite(e.key, e.value), // ScopeRecorder method
40
40
  * onDecision: (e) => logDecision(e.chosen), // FlowRecorder method
41
41
  * };
42
42
  *
@@ -46,7 +46,7 @@
46
46
  *
47
47
  * ## Contract with existing APIs
48
48
  *
49
- * - `attachRecorder(r)` and `attachFlowRecorder(r)` remain unchanged.
49
+ * - `attachScopeRecorder(r)` and `attachFlowRecorder(r)` remain unchanged.
50
50
  * Consumers who want only ONE channel keep using them — explicit is good.
51
51
  * - `attachCombinedRecorder(r)` is the ONLY way to guarantee an object is
52
52
  * hooked into every stream it has methods for, without maintaining two
@@ -55,10 +55,10 @@
55
55
  * same `id` replaces the previous instance on BOTH channels, not just one.
56
56
  */
57
57
  import type { FlowErrorEvent, FlowPauseEvent, FlowRecorder, FlowResumeEvent } from '../engine/narrative/types.js';
58
- import type { ErrorEvent, PauseEvent, Recorder, ResumeEvent } from '../scope/types.js';
58
+ import type { ErrorEvent, PauseEvent, ResumeEvent, ScopeRecorder } from '../scope/types.js';
59
59
  import type { EmitRecorder } from './EmitRecorder.js';
60
60
  /**
61
- * Method names that appear on BOTH `Recorder` and `FlowRecorder` but with
61
+ * Method names that appear on BOTH `ScopeRecorder` and `FlowRecorder` but with
62
62
  * different event payload types. For these, a `CombinedRecorder` declares
63
63
  * ONE handler that receives the union of both payloads — consumers
64
64
  * discriminate on `traversalContext`, which only control-flow events carry.
@@ -70,7 +70,7 @@ type SharedLifecycle = 'id' | 'clear' | 'toSnapshot';
70
70
  * A recorder that MAY observe any combination of supported event streams.
71
71
  *
72
72
  * Today's streams:
73
- * - Scope data-flow (`Recorder`: onRead/onWrite/onCommit/onStageStart/…)
73
+ * - Scope data-flow (`ScopeRecorder`: onRead/onWrite/onCommit/onStageStart/…)
74
74
  * - Control-flow (`FlowRecorder`: onDecision/onSubflowEntry/onLoop/…)
75
75
  *
76
76
  * All event handlers are optional — implement only what you care about.
@@ -78,7 +78,7 @@ type SharedLifecycle = 'id' | 'clear' | 'toSnapshot';
78
78
  *
79
79
  * ## Shared method names (onError / onPause / onResume)
80
80
  *
81
- * Both `Recorder` and `FlowRecorder` declare these with DIFFERENT payload
81
+ * Both `ScopeRecorder` and `FlowRecorder` declare these with DIFFERENT payload
82
82
  * shapes. In a combined recorder, each such handler is called by BOTH
83
83
  * channels with its own variant. The parameter type is a union — consumers
84
84
  * can either handle both variants uniformly, or discriminate (control-flow
@@ -90,7 +90,7 @@ type SharedLifecycle = 'id' | 'clear' | 'toSnapshot';
90
90
  * gains another `& Partial<…>` clause. Because every clause is `Partial`,
91
91
  * existing `CombinedRecorder` implementations remain type-valid.
92
92
  */
93
- export type CombinedRecorder = Partial<Omit<Recorder, SharedLifecycleOverlap | SharedLifecycle>> & Partial<Omit<FlowRecorder, SharedLifecycleOverlap | SharedLifecycle>> & Partial<Omit<EmitRecorder, SharedLifecycle>> & {
93
+ export type CombinedRecorder = Partial<Omit<ScopeRecorder, SharedLifecycleOverlap | SharedLifecycle>> & Partial<Omit<FlowRecorder, SharedLifecycleOverlap | SharedLifecycle>> & Partial<Omit<EmitRecorder, SharedLifecycle>> & {
94
94
  readonly id: string;
95
95
  clear?(): void;
96
96
  toSnapshot?(): {
@@ -151,10 +151,10 @@ export declare function isFlowEvent<T>(event: T): event is Exclude<T, {
151
151
  *
152
152
  * ## Return type
153
153
  *
154
- * Returns plain `boolean` (not a type predicate). The full `Recorder`
154
+ * Returns plain `boolean` (not a type predicate). The full `ScopeRecorder`
155
155
  * interface has payload types that diverge from `CombinedRecorder`'s union
156
156
  * variants for shared methods, so a narrowing predicate would be unsound.
157
- * Callers that need to treat the recorder as a `Recorder` do so explicitly
157
+ * Callers that need to treat the recorder as a `ScopeRecorder` do so explicitly
158
158
  * at the attach site — the cast is the contract that each channel passes
159
159
  * its own payload variant.
160
160
  */