footprintjs 8.3.0 → 9.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.
- package/dist/esm/lib/engine/handlers/ContinuationResolver.js +44 -45
- package/dist/esm/lib/engine/handlers/DeciderHandler.js +37 -14
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +468 -315
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/memory/StageContext.js +28 -7
- package/dist/esm/lib/runner/FlowChartExecutor.js +5 -4
- package/dist/lib/engine/handlers/ContinuationResolver.js +44 -45
- package/dist/lib/engine/handlers/DeciderHandler.js +37 -14
- package/dist/lib/engine/traversal/FlowchartTraverser.js +468 -315
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/memory/StageContext.js +28 -7
- package/dist/lib/runner/FlowChartExecutor.js +5 -4
- package/dist/types/lib/engine/handlers/ContinuationResolver.d.ts +36 -9
- package/dist/types/lib/engine/handlers/DeciderHandler.d.ts +32 -0
- package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +91 -23
- package/dist/types/lib/engine/types.d.ts +15 -3
- package/dist/types/lib/memory/StageContext.d.ts +3 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
* - String ID → reference to existing node (resolve via NodeResolver)
|
|
9
9
|
* - StageNode with fn → truly dynamic node (execute directly)
|
|
10
10
|
* - StageNode without fn → reference by ID (resolve via NodeResolver)
|
|
11
|
+
*
|
|
12
|
+
* Two entry points:
|
|
13
|
+
* - `resolveTarget` — resolves the continuation to `{ node, context }` and
|
|
14
|
+
* fires every side effect (iteration counting, debug logs, `onLoop`
|
|
15
|
+
* narrative) WITHOUT executing. The traverser's trampoline driver uses
|
|
16
|
+
* this to follow loop edges iteratively — flat stack, so the iteration
|
|
17
|
+
* limit (not call-stack depth) is what bounds a loop.
|
|
18
|
+
* - `resolve` — resolveTarget + immediate execution via the provided
|
|
19
|
+
* `executeNode` callback. Kept for direct/advanced callers.
|
|
11
20
|
*/
|
|
12
21
|
import type { StageContext } from '../../memory/StageContext.js';
|
|
13
22
|
import type { StageNode } from '../graph/StageNode.js';
|
|
@@ -16,6 +25,16 @@ import type { HandlerDeps } from '../types.js';
|
|
|
16
25
|
import type { NodeResolver } from './NodeResolver.js';
|
|
17
26
|
import type { ExecuteNodeFn } from './types.js';
|
|
18
27
|
export declare const DEFAULT_MAX_ITERATIONS = 1000;
|
|
28
|
+
/**
|
|
29
|
+
* A resolved continuation target — the node to execute next plus the
|
|
30
|
+
* StageContext to execute it in. All side effects (iteration counting,
|
|
31
|
+
* debug logs, `onLoop` narrative) have already fired by the time this
|
|
32
|
+
* is returned.
|
|
33
|
+
*/
|
|
34
|
+
export interface ResolvedContinuation<TOut = any, TScope = any> {
|
|
35
|
+
node: StageNode<TOut, TScope>;
|
|
36
|
+
context: StageContext;
|
|
37
|
+
}
|
|
19
38
|
export declare class ContinuationResolver<TOut = any, TScope = any> {
|
|
20
39
|
private readonly deps;
|
|
21
40
|
private readonly nodeResolver;
|
|
@@ -28,19 +47,27 @@ export declare class ContinuationResolver<TOut = any, TScope = any> {
|
|
|
28
47
|
private readonly maxIterations;
|
|
29
48
|
constructor(deps: HandlerDeps<TOut, TScope>, nodeResolver: NodeResolver<TOut, TScope>, onIterationUpdate?: (nodeId: string, count: number) => void, maxIterations?: number);
|
|
30
49
|
/**
|
|
31
|
-
* Resolve a dynamic continuation.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
50
|
+
* Resolve a dynamic continuation and execute it immediately.
|
|
51
|
+
* Equivalent to `executeNode(...resolveTarget(...))` — the traverser's
|
|
52
|
+
* driver loop calls `resolveTarget` directly instead so the continuation
|
|
53
|
+
* becomes a flat trampoline hop rather than a retained recursive frame.
|
|
34
54
|
*/
|
|
35
55
|
resolve(dynamicNext: string | StageNode<TOut, TScope>, node: StageNode<TOut, TScope>, context: StageContext, breakFlag: {
|
|
36
56
|
shouldBreak: boolean;
|
|
37
57
|
}, branchPath: string | undefined, executeNode: ExecuteNodeFn<TOut, TScope>, traversalContext?: TraversalContext): Promise<any>;
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a dynamic continuation to its target node + next StageContext
|
|
60
|
+
* WITHOUT executing it. Fires the same side effects `resolve` always did
|
|
61
|
+
* (iteration counting + limit, `dynamicNext*` logs, loop debug message,
|
|
62
|
+
* `onLoop` narrative), in the same order.
|
|
63
|
+
*
|
|
64
|
+
* Three dynamicNext patterns:
|
|
65
|
+
* - StageNode with fn → truly dynamic node, returned as-is (no iteration
|
|
66
|
+
* tracking — it is a fresh node, not a back-edge).
|
|
67
|
+
* - String ID → reference to an existing node, resolved via NodeResolver.
|
|
68
|
+
* - StageNode without fn → reference by ID, resolved via NodeResolver.
|
|
69
|
+
*/
|
|
70
|
+
resolveTarget(dynamicNext: string | StageNode<TOut, TScope>, currentNode: StageNode<TOut, TScope>, context: StageContext, branchPath: string | undefined, traversalContext?: TraversalContext): ResolvedContinuation<TOut, TScope>;
|
|
44
71
|
/**
|
|
45
72
|
* Get the next iteration number for a node and increment.
|
|
46
73
|
* Returns 0 for first visit, 1 for second, etc.
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles scope-based deciders (stage IS the decider, returns branch ID).
|
|
5
5
|
* Logs flow control decisions and narrative sentences.
|
|
6
|
+
*
|
|
7
|
+
* Two entry points:
|
|
8
|
+
* - `prepareDispatch` — runs the decider stage, commits, resolves the chosen
|
|
9
|
+
* branch, fires narrative, and returns the chosen node + branch context
|
|
10
|
+
* WITHOUT executing it. The traverser's trampoline driver uses this so a
|
|
11
|
+
* decider with no continuation of its own can hand the branch to the
|
|
12
|
+
* driver as a flat hop (loop-heavy decider charts stay flat-stacked).
|
|
13
|
+
* - `handleScopeBased` — prepareDispatch + immediate branch execution via
|
|
14
|
+
* the provided `executeNode` callback. Kept for direct/advanced callers
|
|
15
|
+
* and for deciders whose own `.next` must run after the branch completes.
|
|
6
16
|
*/
|
|
7
17
|
import type { StageContext } from '../../memory/StageContext.js';
|
|
8
18
|
import type { StageNode } from '../graph/StageNode.js';
|
|
@@ -10,6 +20,19 @@ import type { TraversalContext } from '../narrative/types.js';
|
|
|
10
20
|
import type { HandlerDeps, StageFunction } from '../types.js';
|
|
11
21
|
import type { ExecuteNodeFn, RunStageFn } from './types.js';
|
|
12
22
|
export type { ExecuteNodeFn, RunStageFn };
|
|
23
|
+
/**
|
|
24
|
+
* Result of `prepareDispatch` — either the decider stage broke (no branch
|
|
25
|
+
* runs; `branchId` is the decider's return value), or a branch was chosen
|
|
26
|
+
* and is ready to execute in `branchContext`.
|
|
27
|
+
*/
|
|
28
|
+
export type DeciderDispatch<TOut = any, TScope = any> = {
|
|
29
|
+
kind: 'break';
|
|
30
|
+
branchId: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'dispatch';
|
|
33
|
+
chosen: StageNode<TOut, TScope>;
|
|
34
|
+
branchContext: StageContext;
|
|
35
|
+
};
|
|
13
36
|
export declare class DeciderHandler<TOut = any, TScope = any> {
|
|
14
37
|
private readonly deps;
|
|
15
38
|
constructor(deps: HandlerDeps<TOut, TScope>);
|
|
@@ -21,4 +44,13 @@ export declare class DeciderHandler<TOut = any, TScope = any> {
|
|
|
21
44
|
handleScopeBased(node: StageNode<TOut, TScope>, stageFunc: StageFunction<TOut, TScope>, context: StageContext, breakFlag: {
|
|
22
45
|
shouldBreak: boolean;
|
|
23
46
|
}, branchPath: string | undefined, runStage: RunStageFn<TOut, TScope>, executeNode: ExecuteNodeFn<TOut, TScope>, traversalContext?: TraversalContext): Promise<any>;
|
|
47
|
+
/**
|
|
48
|
+
* Run the decider stage and resolve the chosen branch WITHOUT executing it.
|
|
49
|
+
* Everything up to (and including) the `onDecision`/`onStageExecuted`
|
|
50
|
+
* narrative and the branch context creation happens here — only the
|
|
51
|
+
* branch execution itself is left to the caller.
|
|
52
|
+
*/
|
|
53
|
+
prepareDispatch(node: StageNode<TOut, TScope>, stageFunc: StageFunction<TOut, TScope>, context: StageContext, breakFlag: {
|
|
54
|
+
shouldBreak: boolean;
|
|
55
|
+
}, branchPath: string | undefined, runStage: RunStageFn<TOut, TScope>, traversalContext?: TraversalContext): Promise<DeciderDispatch<TOut, TScope>>;
|
|
24
56
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FlowchartTraverser — Pre-order DFS traversal of StageNode graph.
|
|
3
3
|
*
|
|
4
|
-
* Unified traversal algorithm for all node shapes
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Unified traversal algorithm for all node shapes. `executeNode` is a
|
|
5
|
+
* TRAMPOLINE driver: it runs `executeNodeStep` (one node, all 7 phases) in
|
|
6
|
+
* a flat loop, following tail continuations (linear `next`, loop edges,
|
|
7
|
+
* dynamic next, flat decider dispatch) iteratively — so chain length and
|
|
8
|
+
* loop iterations never grow the call stack. Only true tree nesting (fork
|
|
9
|
+
* children, with-continuation decider/selector branches, subflow mounts)
|
|
10
|
+
* recurses.
|
|
8
11
|
*
|
|
9
|
-
* For each node,
|
|
12
|
+
* For each node, executeNodeStep follows 7 phases:
|
|
10
13
|
* 0. CLASSIFY — subflow detection, early delegation
|
|
11
14
|
* 1. VALIDATE — node invariants, role markers
|
|
12
15
|
* 2. EXECUTE — run stage fn, commit, break check
|
|
@@ -49,10 +52,17 @@ export interface TraverserOptions<TOut = any, TScope = any> {
|
|
|
49
52
|
*/
|
|
50
53
|
narrativeGenerator?: IControlFlowNarrative;
|
|
51
54
|
/**
|
|
52
|
-
* Maximum
|
|
53
|
-
*
|
|
55
|
+
* Maximum nested executeNode depth (tree nesting — branch/fork dispatch and
|
|
56
|
+
* dynamic recursion, NOT linear chains or loop iterations, which run flat).
|
|
57
|
+
* Defaults to FlowchartTraverser.MAX_EXECUTE_DEPTH (500).
|
|
54
58
|
*/
|
|
55
59
|
maxDepth?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Maximum loop iterations per node (the ContinuationResolver guard).
|
|
62
|
+
* Defaults to DEFAULT_MAX_ITERATIONS (1000). Propagated to subflow
|
|
63
|
+
* traversers. Must be >= 1.
|
|
64
|
+
*/
|
|
65
|
+
maxIterations?: number;
|
|
56
66
|
/**
|
|
57
67
|
* When this traverser runs inside a subflow, set this to the subflow's ID.
|
|
58
68
|
* Propagated to TraversalContext so narrative entries carry the correct subflowId.
|
|
@@ -158,11 +168,26 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
158
168
|
private readonly dynamicPatches;
|
|
159
169
|
private patchCount;
|
|
160
170
|
/**
|
|
161
|
-
*
|
|
162
|
-
* Each
|
|
163
|
-
*
|
|
171
|
+
* TREE-nesting depth counter for executeNode (the trampoline driver).
|
|
172
|
+
* Each driver invocation increments this; decrements on exit (try/finally).
|
|
173
|
+
*
|
|
174
|
+
* Linear `next` chains, loop edges, and dynamic continuations are followed
|
|
175
|
+
* ITERATIVELY inside one driver invocation, so they never grow this
|
|
176
|
+
* counter. Only true tree recursion does: fork children, decider/selector
|
|
177
|
+
* branch dispatch (when the decider has its own continuation), and
|
|
178
|
+
* unbounded dynamic recursion. Prevents call-stack overflow on runaway
|
|
179
|
+
* recursive composition.
|
|
164
180
|
*/
|
|
165
181
|
private _executeDepth;
|
|
182
|
+
/**
|
|
183
|
+
* Memoized parent-chain depth per StageContext. The context tree deepens
|
|
184
|
+
* by one per executed stage along a chain, so the naive parent-walk in
|
|
185
|
+
* `computeContextDepth` is O(chain length) per stage — O(n²) per run once
|
|
186
|
+
* the trampoline allows chains of tens of thousands of stages. Contexts
|
|
187
|
+
* are visited parent-before-child, so the memo makes each lookup O(1)
|
|
188
|
+
* amortized. WeakMap — dies with the traverser.
|
|
189
|
+
*/
|
|
190
|
+
private readonly contextDepthCache;
|
|
166
191
|
/**
|
|
167
192
|
* Shared mutable execution counter — monotonic, incremented per stage execution.
|
|
168
193
|
* Shared with child traversers (subflows) so indices are globally unique within a run.
|
|
@@ -173,18 +198,26 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
173
198
|
*/
|
|
174
199
|
private readonly _maxDepth;
|
|
175
200
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
201
|
+
* Per-instance loop-iteration limit forwarded to the ContinuationResolver
|
|
202
|
+
* and propagated to subflow traversers. Undefined → resolver default (1000).
|
|
203
|
+
*/
|
|
204
|
+
private readonly _maxIterations?;
|
|
205
|
+
/**
|
|
206
|
+
* Default maximum nested executeNode depth before an error is thrown.
|
|
179
207
|
*
|
|
180
|
-
* **
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
208
|
+
* **What counts as depth (trampoline model):** `executeNode` is an iterative
|
|
209
|
+
* driver — linear `next` hops, loop edges (`loopTo`/dynamic next), and
|
|
210
|
+
* dynamic-subflow re-entry are followed in a flat loop and consume NO depth.
|
|
211
|
+
* Depth grows only with true tree nesting: one tick per fork child, one per
|
|
212
|
+
* decider/selector branch dispatch that must return to its invoker (decider
|
|
213
|
+
* with its own `next`), one per subflow mount frame in the parent (the
|
|
214
|
+
* subflow body itself runs on a FRESH traverser with its own budget).
|
|
184
215
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
216
|
+
* 500 therefore covers any realistic chart — it bounds recursive
|
|
217
|
+
* COMPOSITION, not chain length or loop count. Loops are bounded by
|
|
218
|
+
* `ContinuationResolver`'s independent iteration limit (default 1000,
|
|
219
|
+
* configurable via `RunOptions.maxIterations`), which is now the binding
|
|
220
|
+
* constraint for loop-heavy pipelines.
|
|
188
221
|
*
|
|
189
222
|
* @remarks Not safe for concurrent `.execute()` calls on the same instance — concurrent
|
|
190
223
|
* executions race on `_executeDepth`. Use a separate `FlowchartTraverser` per concurrent
|
|
@@ -243,7 +276,9 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
243
276
|
/**
|
|
244
277
|
* Build an O(1) ID→node map from the root graph.
|
|
245
278
|
* Used by NodeResolver to avoid repeated DFS on every loopTo() call.
|
|
246
|
-
*
|
|
279
|
+
* Iterative worklist (no recursion) so arbitrarily long chains index fully;
|
|
280
|
+
* the `map.has` guard handles cyclic refs. First-visited node wins per ID —
|
|
281
|
+
* worklist order matches the old recursive pre-order (children, then next).
|
|
247
282
|
* Dynamic subflows and lazy-resolved nodes are added to stageMap at runtime but not to this map —
|
|
248
283
|
* those use the DFS fallback in NodeResolver.
|
|
249
284
|
*/
|
|
@@ -269,11 +304,44 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
269
304
|
private effNode;
|
|
270
305
|
private executeStage;
|
|
271
306
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
307
|
+
* Trampoline driver — pre-order DFS traversal entry point.
|
|
308
|
+
*
|
|
309
|
+
* Runs `executeNodeStep` (one node, all 7 phases) in a flat loop: every
|
|
310
|
+
* TAIL continuation (linear `next`, loop edge, dynamic next / dynamic
|
|
311
|
+
* re-entry, no-continuation decider dispatch) comes back as a
|
|
312
|
+
* `ContinuationHop` and is followed ITERATIVELY — neither the call stack
|
|
313
|
+
* nor the retained promise chain grows with chain length or loop count.
|
|
314
|
+
*
|
|
315
|
+
* Recursion remains ONLY for true tree nesting (each gets a nested driver
|
|
316
|
+
* call): fork children (`ChildrenExecutor`), selector branches (parallel
|
|
317
|
+
* fan-out), decider branch dispatch when the decider has its own `next`
|
|
318
|
+
* (the branch must complete BEFORE the decider's continuation runs), and
|
|
319
|
+
* subflow mounts (fresh traverser; the mount frame stays in the parent).
|
|
320
|
+
* `_executeDepth` therefore counts chart COMPOSITION depth only, guarded
|
|
321
|
+
* by `_maxDepth` (default `MAX_EXECUTE_DEPTH` = 500).
|
|
322
|
+
*
|
|
323
|
+
* PauseSignal: a flat decider dispatch records an `InvokerStamp`; if the
|
|
324
|
+
* continued chain later pauses, the driver stamps the signal during
|
|
325
|
+
* unwind — same invoker context the recursive dispatch's catch used to
|
|
326
|
+
* stamp, innermost (most recent dispatch) first.
|
|
274
327
|
*/
|
|
275
328
|
private executeNode;
|
|
329
|
+
/** Build a flat continuation hop for the driver loop. */
|
|
330
|
+
private hop;
|
|
331
|
+
/**
|
|
332
|
+
* Execute ONE node through all 7 phases — the old recursive `executeNode`
|
|
333
|
+
* body; only the tail calls became `ContinuationHop` returns. Returns the
|
|
334
|
+
* node's result, or a hop for the driver loop to follow.
|
|
335
|
+
*/
|
|
336
|
+
private executeNodeStep;
|
|
276
337
|
private captureDynamicChildrenResult;
|
|
338
|
+
/**
|
|
339
|
+
* Parent-chain length of a StageContext — same value the pre-trampoline
|
|
340
|
+
* walk produced, memoized. The context tree deepens by one per executed
|
|
341
|
+
* stage along a chain, so the naive walk is O(chain length) per stage —
|
|
342
|
+
* O(n²) per run once chains reach trampoline scale. Contexts are visited
|
|
343
|
+
* parent-before-child, so the cached parent makes this O(1) amortized.
|
|
344
|
+
*/
|
|
277
345
|
private computeContextDepth;
|
|
278
346
|
private prefixNodeTree;
|
|
279
347
|
private autoRegisterSubflowDef;
|
|
@@ -322,12 +322,24 @@ export interface RunOptions {
|
|
|
322
322
|
*/
|
|
323
323
|
env?: ExecutionEnv;
|
|
324
324
|
/**
|
|
325
|
-
* Override the maximum
|
|
325
|
+
* Override the maximum nested `executeNode` depth for this run.
|
|
326
326
|
* Defaults to `FlowchartTraverser.MAX_EXECUTE_DEPTH` (500).
|
|
327
|
-
*
|
|
328
|
-
*
|
|
327
|
+
*
|
|
328
|
+
* Depth counts TREE nesting only — fork children, decider/selector branch
|
|
329
|
+
* dispatch, recursive composition. Linear chains and loop iterations run
|
|
330
|
+
* on a flat trampoline and never consume depth, so this rarely needs
|
|
331
|
+
* raising. Must be >= 1.
|
|
329
332
|
*/
|
|
330
333
|
maxDepth?: number;
|
|
334
|
+
/**
|
|
335
|
+
* Override the maximum loop iterations per node for this run (the
|
|
336
|
+
* `ContinuationResolver` infinite-loop guard). Defaults to 1000.
|
|
337
|
+
* This is the binding constraint for loop-heavy pipelines — raise it for
|
|
338
|
+
* legitimately long loops (`loopTo` chains run with a flat stack, so high
|
|
339
|
+
* values are safe; memory for state/narrative still grows per iteration).
|
|
340
|
+
* Propagates to subflows. Must be >= 1.
|
|
341
|
+
*/
|
|
342
|
+
maxIterations?: number;
|
|
331
343
|
}
|
|
332
344
|
export type { FlowControlType, FlowMessage };
|
|
333
345
|
export interface RuntimeStructureMetadata {
|
|
@@ -117,4 +117,7 @@ export declare class StageContext {
|
|
|
117
117
|
}): void;
|
|
118
118
|
getStageId(): string;
|
|
119
119
|
getSnapshot(): StageSnapshot;
|
|
120
|
+
/** Snapshot of THIS context's own fields — `next`/`children` are filled
|
|
121
|
+
* in by the iterative walk in `getSnapshot`. */
|
|
122
|
+
private snapshotSelf;
|
|
120
123
|
}
|
|
@@ -212,7 +212,7 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
212
212
|
* const result = await executor.resume(restored, { approved: true });
|
|
213
213
|
* ```
|
|
214
214
|
*/
|
|
215
|
-
resume(checkpoint: FlowchartCheckpoint, resumeInput?: unknown, options?: Pick<RunOptions, 'signal' | 'env' | 'maxDepth'>): Promise<ExecutorResult>;
|
|
215
|
+
resume(checkpoint: FlowchartCheckpoint, resumeInput?: unknown, options?: Pick<RunOptions, 'signal' | 'env' | 'maxDepth' | 'maxIterations'>): Promise<ExecutorResult>;
|
|
216
216
|
/**
|
|
217
217
|
* Build a fully DETACHED checkpoint from a caught PauseSignal.
|
|
218
218
|
*
|
package/package.json
CHANGED