footprintjs 4.0.5 → 4.2.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/CLAUDE.md +7 -0
- package/dist/esm/index.js +6 -1
- package/dist/esm/lib/builder/FlowChartBuilder.js +13 -8
- package/dist/esm/lib/engine/graph/StageNode.js +1 -1
- package/dist/esm/lib/engine/handlers/DeciderHandler.js +8 -7
- package/dist/esm/lib/engine/handlers/SelectorHandler.js +8 -5
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +28 -246
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +232 -139
- package/dist/esm/lib/engine/narrative/index.js +1 -1
- package/dist/esm/lib/engine/narrative/narrativeTypes.js +1 -1
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +64 -22
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/memory/StageContext.js +36 -9
- package/dist/esm/lib/reactive/createTypedScope.js +9 -6
- package/dist/esm/lib/reactive/types.js +1 -1
- package/dist/esm/lib/recorder/CompositeRecorder.js +172 -0
- package/dist/esm/lib/recorder/index.js +2 -0
- package/dist/esm/lib/runner/FlowChartExecutor.js +33 -3
- package/dist/esm/lib/scope/ScopeFacade.js +52 -4
- package/dist/esm/lib/scope/recorders/DebugRecorder.js +18 -2
- package/dist/esm/lib/scope/recorders/MetricRecorder.js +51 -4
- package/dist/esm/recorders.js +6 -5
- package/dist/index.js +21 -15
- package/dist/lib/builder/FlowChartBuilder.js +13 -8
- package/dist/lib/engine/graph/StageNode.js +1 -1
- package/dist/lib/engine/handlers/DeciderHandler.js +8 -7
- package/dist/lib/engine/handlers/SelectorHandler.js +8 -5
- package/dist/lib/engine/handlers/SubflowExecutor.js +27 -245
- package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +232 -139
- package/dist/lib/engine/narrative/index.js +1 -1
- package/dist/lib/engine/narrative/narrativeTypes.js +1 -1
- package/dist/lib/engine/traversal/FlowchartTraverser.js +64 -22
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/memory/StageContext.js +36 -9
- package/dist/lib/reactive/createTypedScope.js +9 -6
- package/dist/lib/reactive/types.js +1 -1
- package/dist/lib/recorder/CompositeRecorder.js +176 -0
- package/dist/lib/recorder/index.js +6 -0
- package/dist/lib/runner/FlowChartExecutor.js +33 -3
- package/dist/lib/scope/ScopeFacade.js +52 -4
- package/dist/lib/scope/recorders/DebugRecorder.js +18 -2
- package/dist/lib/scope/recorders/MetricRecorder.js +51 -4
- package/dist/recorders.js +8 -6
- package/dist/types/index.d.ts +4 -0
- package/dist/types/lib/engine/graph/StageNode.d.ts +4 -0
- package/dist/types/lib/engine/handlers/SubflowExecutor.d.ts +10 -29
- package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +18 -1
- package/dist/types/lib/engine/narrative/index.d.ts +1 -1
- package/dist/types/lib/engine/narrative/narrativeTypes.d.ts +92 -0
- package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +20 -8
- package/dist/types/lib/engine/types.d.ts +51 -0
- package/dist/types/lib/memory/StageContext.d.ts +14 -3
- package/dist/types/lib/reactive/types.d.ts +2 -0
- package/dist/types/lib/recorder/CompositeRecorder.d.ts +95 -0
- package/dist/types/lib/recorder/index.d.ts +2 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +27 -1
- package/dist/types/lib/scope/ScopeFacade.d.ts +11 -0
- package/dist/types/lib/scope/recorders/DebugRecorder.d.ts +16 -0
- package/dist/types/lib/scope/recorders/MetricRecorder.d.ts +47 -2
- package/dist/types/recorders.d.ts +9 -4
- package/package.json +1 -1
|
@@ -1,54 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SubflowExecutor —
|
|
2
|
+
* SubflowExecutor — Isolation boundary for subflow execution.
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities:
|
|
5
|
-
* -
|
|
5
|
+
* - Create isolated ExecutionRuntime for each subflow
|
|
6
6
|
* - Apply input/output mapping via SubflowInputMapper
|
|
7
|
-
* -
|
|
7
|
+
* - Delegate traversal to a factory-created FlowchartTraverser
|
|
8
8
|
* - Track subflow results for debugging/visualization
|
|
9
9
|
*
|
|
10
10
|
* Each subflow gets its own GlobalStore for isolation.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Traversal uses the SAME 7-phase algorithm as the top-level traverser
|
|
12
|
+
* (via SubflowTraverserFactory), so deciders, selectors, loops, lazy subflows,
|
|
13
|
+
* and abort signals all work inside subflows automatically.
|
|
13
14
|
*/
|
|
14
15
|
import type { StageContext } from '../../memory/StageContext.js';
|
|
15
16
|
import type { StageNode } from '../graph/StageNode.js';
|
|
16
17
|
import type { TraversalContext } from '../narrative/types.js';
|
|
17
|
-
import type { HandlerDeps,
|
|
18
|
-
import type { NodeResolver } from './NodeResolver.js';
|
|
19
|
-
import type { CallExtractorFn, RunStageFn } from './types.js';
|
|
20
|
-
export type { CallExtractorFn, RunStageFn } from './types.js';
|
|
21
|
-
/** Callback for getting a stage function from the stage map. */
|
|
22
|
-
export type GetStageFnFn<TOut = any, TScope = any> = (node: StageNode<TOut, TScope>) => StageFunction<TOut, TScope> | undefined;
|
|
18
|
+
import type { HandlerDeps, SubflowResult, SubflowTraverserFactory } from '../types.js';
|
|
23
19
|
export declare class SubflowExecutor<TOut = any, TScope = any> {
|
|
24
20
|
private deps;
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
private callExtractor;
|
|
28
|
-
private getStageFn;
|
|
29
|
-
private currentSubflowDeps?;
|
|
30
|
-
private currentSubflowRoot?;
|
|
31
|
-
private subflowResultsMap?;
|
|
32
|
-
constructor(deps: HandlerDeps<TOut, TScope>, nodeResolver: NodeResolver<TOut, TScope>, executeStage: RunStageFn<TOut, TScope>, callExtractor: CallExtractorFn<TOut, TScope>, getStageFn: GetStageFnFn<TOut, TScope>);
|
|
21
|
+
private traverserFactory;
|
|
22
|
+
constructor(deps: HandlerDeps<TOut, TScope>, traverserFactory: SubflowTraverserFactory<TOut, TScope>);
|
|
33
23
|
/**
|
|
34
24
|
* Execute a subflow with isolated context.
|
|
35
25
|
*
|
|
36
26
|
* 1. Creates a fresh ExecutionRuntime for the subflow
|
|
37
27
|
* 2. Applies input mapping to seed the subflow's GlobalStore
|
|
38
|
-
* 3.
|
|
28
|
+
* 3. Delegates traversal to a factory-created FlowchartTraverser
|
|
39
29
|
* 4. Applies output mapping to write results back to parent scope
|
|
40
30
|
* 5. Stores execution data for debugging/visualization
|
|
41
31
|
*/
|
|
42
32
|
executeSubflow(node: StageNode<TOut, TScope>, parentContext: StageContext, breakFlag: {
|
|
43
33
|
shouldBreak: boolean;
|
|
44
34
|
}, branchPath: string | undefined, subflowResultsMap: Map<string, SubflowResult>, parentTraversalContext?: TraversalContext): Promise<any>;
|
|
45
|
-
/**
|
|
46
|
-
* Internal execution within subflow context.
|
|
47
|
-
* Mirrors the traverser's executeNode but within the subflow's isolated runtime.
|
|
48
|
-
*/
|
|
49
|
-
private executeSubflowInternal;
|
|
50
|
-
private computeContextDepth;
|
|
51
|
-
private getStagePath;
|
|
52
|
-
private executeNodeChildrenInternal;
|
|
53
|
-
private executeSelectedChildrenInternal;
|
|
54
35
|
}
|
|
@@ -13,12 +13,18 @@
|
|
|
13
13
|
* emit the stage entry + flush the buffered ops in one pass.
|
|
14
14
|
*/
|
|
15
15
|
import type { ReadEvent, Recorder, WriteEvent } from '../../scope/types.js';
|
|
16
|
-
import type { CombinedNarrativeEntry } from './narrativeTypes.js';
|
|
16
|
+
import type { CombinedNarrativeEntry, NarrativeRenderer } from './narrativeTypes.js';
|
|
17
17
|
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowRecorder, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent } from './types.js';
|
|
18
18
|
export interface CombinedNarrativeRecorderOptions {
|
|
19
19
|
includeStepNumbers?: boolean;
|
|
20
20
|
includeValues?: boolean;
|
|
21
21
|
maxValueLength?: number;
|
|
22
|
+
/** Custom value formatter. Called at render time (flushOps), not capture time.
|
|
23
|
+
* Receives the raw value and maxValueLength. Defaults to summarizeValue(). */
|
|
24
|
+
formatValue?: (value: unknown, maxLen: number) => string;
|
|
25
|
+
/** Pluggable renderer for customizing narrative output. Unimplemented methods
|
|
26
|
+
* fall back to the default English renderer. See NarrativeRenderer docs. */
|
|
27
|
+
renderer?: NarrativeRenderer;
|
|
22
28
|
}
|
|
23
29
|
export declare class CombinedNarrativeRecorder implements FlowRecorder, Recorder {
|
|
24
30
|
readonly id: string;
|
|
@@ -39,6 +45,8 @@ export declare class CombinedNarrativeRecorder implements FlowRecorder, Recorder
|
|
|
39
45
|
private includeStepNumbers;
|
|
40
46
|
private includeValues;
|
|
41
47
|
private maxValueLength;
|
|
48
|
+
private formatValue;
|
|
49
|
+
private renderer?;
|
|
42
50
|
constructor(options?: CombinedNarrativeRecorderOptions & {
|
|
43
51
|
id?: string;
|
|
44
52
|
});
|
|
@@ -79,4 +87,13 @@ export declare class CombinedNarrativeRecorder implements FlowRecorder, Recorder
|
|
|
79
87
|
private consumeFirstStageFlag;
|
|
80
88
|
private bufferOp;
|
|
81
89
|
private flushOps;
|
|
90
|
+
private defaultRenderStage;
|
|
91
|
+
private defaultRenderOp;
|
|
92
|
+
private defaultRenderDecision;
|
|
93
|
+
private defaultRenderFork;
|
|
94
|
+
private defaultRenderSelected;
|
|
95
|
+
private defaultRenderSubflow;
|
|
96
|
+
private defaultRenderLoop;
|
|
97
|
+
private defaultRenderBreak;
|
|
98
|
+
private defaultRenderError;
|
|
82
99
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { CombinedNarrativeRecorderOptions } from './CombinedNarrativeRecorder.js';
|
|
2
2
|
export { CombinedNarrativeRecorder } from './CombinedNarrativeRecorder.js';
|
|
3
|
-
export type { CombinedNarrativeEntry, CombinedNarrativeOptions } from './narrativeTypes.js';
|
|
3
|
+
export type { BreakRenderContext, CombinedNarrativeEntry, CombinedNarrativeOptions, DecisionRenderContext, ErrorRenderContext, ForkRenderContext, LoopRenderContext, NarrativeRenderer, OpRenderContext, SelectedRenderContext, StageRenderContext, SubflowRenderContext, } from './narrativeTypes.js';
|
|
4
4
|
export { NullControlFlowNarrativeGenerator } from './NullControlFlowNarrativeGenerator.js';
|
|
5
5
|
export type { IControlFlowNarrative } from './types.js';
|
|
6
6
|
export { FlowRecorderDispatcher } from './FlowRecorderDispatcher.js';
|
|
@@ -15,9 +15,101 @@ export interface CombinedNarrativeEntry {
|
|
|
15
15
|
stepNumber?: number;
|
|
16
16
|
/** Subflow ID when this entry was generated inside a subflow. Undefined for root-level. */
|
|
17
17
|
subflowId?: string;
|
|
18
|
+
/** Raw value from the scope event — available for programmatic access and custom formatting.
|
|
19
|
+
* Only present on 'step' entries (read/write ops). This is a live reference, not a clone.
|
|
20
|
+
* When using ScopeFacade, redacted keys will have value '[REDACTED]' (sanitized upstream
|
|
21
|
+
* by ScopeFacade before dispatching events — the recorder does not enforce redaction). */
|
|
22
|
+
rawValue?: unknown;
|
|
18
23
|
}
|
|
19
24
|
export interface CombinedNarrativeOptions {
|
|
20
25
|
includeStepNumbers?: boolean;
|
|
21
26
|
includeValues?: boolean;
|
|
22
27
|
indent?: string;
|
|
23
28
|
}
|
|
29
|
+
/** Context passed to renderStage. */
|
|
30
|
+
export interface StageRenderContext {
|
|
31
|
+
stageName: string;
|
|
32
|
+
stageNumber: number;
|
|
33
|
+
isFirst: boolean;
|
|
34
|
+
description?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Context passed to renderOp. Return null to exclude the entry. */
|
|
37
|
+
export interface OpRenderContext {
|
|
38
|
+
type: 'read' | 'write';
|
|
39
|
+
key: string;
|
|
40
|
+
rawValue: unknown;
|
|
41
|
+
valueSummary: string;
|
|
42
|
+
operation?: 'set' | 'update' | 'delete';
|
|
43
|
+
stepNumber: number;
|
|
44
|
+
}
|
|
45
|
+
/** Context passed to renderDecision. */
|
|
46
|
+
export interface DecisionRenderContext {
|
|
47
|
+
decider: string;
|
|
48
|
+
chosen: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
rationale?: string;
|
|
51
|
+
/** Raw evidence from decide()/select() — available for custom rendering. */
|
|
52
|
+
evidence?: unknown;
|
|
53
|
+
}
|
|
54
|
+
/** Context passed to renderFork. */
|
|
55
|
+
export interface ForkRenderContext {
|
|
56
|
+
children: string[];
|
|
57
|
+
}
|
|
58
|
+
/** Context passed to renderSubflow. */
|
|
59
|
+
export interface SubflowRenderContext {
|
|
60
|
+
name: string;
|
|
61
|
+
direction: 'entry' | 'exit';
|
|
62
|
+
description?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Context passed to renderLoop. */
|
|
65
|
+
export interface LoopRenderContext {
|
|
66
|
+
target: string;
|
|
67
|
+
iteration: number;
|
|
68
|
+
description?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Context passed to renderBreak. */
|
|
71
|
+
export interface BreakRenderContext {
|
|
72
|
+
stageName: string;
|
|
73
|
+
}
|
|
74
|
+
/** Context passed to renderSelected. */
|
|
75
|
+
export interface SelectedRenderContext {
|
|
76
|
+
selected: string[];
|
|
77
|
+
total: number;
|
|
78
|
+
/** Raw evidence from select() — available for custom rendering. */
|
|
79
|
+
evidence?: unknown;
|
|
80
|
+
}
|
|
81
|
+
/** Context passed to renderError. */
|
|
82
|
+
export interface ErrorRenderContext {
|
|
83
|
+
stageName: string;
|
|
84
|
+
message: string;
|
|
85
|
+
validationIssues?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Pluggable renderer for customizing narrative output.
|
|
89
|
+
*
|
|
90
|
+
* Each method is optional — unimplemented methods fall back to the default
|
|
91
|
+
* English renderer. Return null from renderOp to exclude an entry entirely.
|
|
92
|
+
*
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const rec = narrative({
|
|
95
|
+
* renderer: {
|
|
96
|
+
* renderOp(ctx) {
|
|
97
|
+
* if (ctx.key.startsWith('_internal')) return null; // filter out internal keys
|
|
98
|
+
* return `${ctx.type === 'read' ? 'Read' : 'Wrote'} ${ctx.key}: ${ctx.valueSummary}`;
|
|
99
|
+
* },
|
|
100
|
+
* },
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export interface NarrativeRenderer {
|
|
105
|
+
renderStage?(ctx: StageRenderContext): string;
|
|
106
|
+
/** Return null to exclude the op from the narrative. */
|
|
107
|
+
renderOp?(ctx: OpRenderContext): string | null;
|
|
108
|
+
renderDecision?(ctx: DecisionRenderContext): string;
|
|
109
|
+
renderFork?(ctx: ForkRenderContext): string;
|
|
110
|
+
renderSelected?(ctx: SelectedRenderContext): string;
|
|
111
|
+
renderSubflow?(ctx: SubflowRenderContext): string;
|
|
112
|
+
renderLoop?(ctx: LoopRenderContext): string;
|
|
113
|
+
renderBreak?(ctx: BreakRenderContext): string;
|
|
114
|
+
renderError?(ctx: ErrorRenderContext): string;
|
|
115
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
/// <reference types="node" />
|
|
22
22
|
import type { ScopeProtectionMode } from '../../scope/protection/types.js';
|
|
23
23
|
import { FlowRecorderDispatcher } from '../narrative/FlowRecorderDispatcher.js';
|
|
24
|
-
import type { FlowRecorder } from '../narrative/types.js';
|
|
24
|
+
import type { FlowRecorder, IControlFlowNarrative } from '../narrative/types.js';
|
|
25
25
|
import type { ExtractorError, IExecutionRuntime, ILogger, ScopeFactory, SerializedPipelineStructure, StageFunction, StageNode, StreamHandlers, SubflowResult, TraversalExtractor, TraversalResult } from '../types.js';
|
|
26
26
|
export interface TraverserOptions<TOut = any, TScope = any> {
|
|
27
27
|
root: StageNode<TOut, TScope>;
|
|
@@ -45,11 +45,22 @@ export interface TraverserOptions<TOut = any, TScope = any> {
|
|
|
45
45
|
signal?: AbortSignal;
|
|
46
46
|
/** Pre-configured FlowRecorders to attach when narrative is enabled. */
|
|
47
47
|
flowRecorders?: FlowRecorder[];
|
|
48
|
+
/**
|
|
49
|
+
* Pre-configured narrative generator. If provided, takes precedence over
|
|
50
|
+
* flowRecorders and narrativeEnabled. Used by the subflow traverser factory
|
|
51
|
+
* to share the parent's narrative generator with subflow traversers.
|
|
52
|
+
*/
|
|
53
|
+
narrativeGenerator?: IControlFlowNarrative;
|
|
48
54
|
/**
|
|
49
55
|
* Maximum recursive executeNode depth. Defaults to FlowchartTraverser.MAX_EXECUTE_DEPTH (500).
|
|
50
56
|
* Override in tests or unusually deep pipelines.
|
|
51
57
|
*/
|
|
52
58
|
maxDepth?: number;
|
|
59
|
+
/**
|
|
60
|
+
* When this traverser runs inside a subflow, set this to the subflow's ID.
|
|
61
|
+
* Propagated to TraversalContext so narrative entries carry the correct subflowId.
|
|
62
|
+
*/
|
|
63
|
+
parentSubflowId?: string;
|
|
53
64
|
}
|
|
54
65
|
export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
55
66
|
private readonly root;
|
|
@@ -58,6 +69,7 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
58
69
|
private subflows;
|
|
59
70
|
private readonly logger;
|
|
60
71
|
private readonly signal?;
|
|
72
|
+
private readonly parentSubflowId?;
|
|
61
73
|
private readonly nodeResolver;
|
|
62
74
|
private readonly childrenExecutor;
|
|
63
75
|
private readonly subflowExecutor;
|
|
@@ -107,8 +119,14 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
107
119
|
*/
|
|
108
120
|
static readonly MAX_EXECUTE_DEPTH = 500;
|
|
109
121
|
constructor(opts: TraverserOptions<TOut, TScope>);
|
|
122
|
+
/**
|
|
123
|
+
* Create a factory that produces FlowchartTraverser instances for subflow execution.
|
|
124
|
+
* Captures parent config in closure — SubflowExecutor provides subflow-specific overrides.
|
|
125
|
+
* Each subflow gets a full traverser with all 7 phases (deciders, selectors, loops, etc.).
|
|
126
|
+
*/
|
|
127
|
+
private createSubflowTraverserFactory;
|
|
110
128
|
private createDeps;
|
|
111
|
-
execute(): Promise<TraversalResult>;
|
|
129
|
+
execute(branchPath?: string): Promise<TraversalResult>;
|
|
112
130
|
getRuntimeStructure(): SerializedPipelineStructure | undefined;
|
|
113
131
|
getSnapshot(): {
|
|
114
132
|
sharedState: Record<string, unknown>;
|
|
@@ -117,12 +135,6 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
117
135
|
subflowResults?: Record<string, unknown> | undefined;
|
|
118
136
|
recorders?: {
|
|
119
137
|
id: string;
|
|
120
|
-
/**
|
|
121
|
-
* Per-traverser set of lazy subflow IDs that have been resolved by THIS run.
|
|
122
|
-
* Used instead of writing `node.subflowResolver = undefined` back to the shared
|
|
123
|
-
* StageNode graph — avoids a race where a concurrent traverser clears the shared
|
|
124
|
-
* resolver before another traverser has finished using it.
|
|
125
|
-
*/
|
|
126
138
|
name: string;
|
|
127
139
|
data: unknown;
|
|
128
140
|
}[] | undefined;
|
|
@@ -41,6 +41,26 @@ export interface StreamHandlers {
|
|
|
41
41
|
}
|
|
42
42
|
export interface SubflowMountOptions<TParentScope = any, TSubflowInput = any, TSubflowOutput = any> {
|
|
43
43
|
inputMapper?: (parentScope: TParentScope) => TSubflowInput;
|
|
44
|
+
/**
|
|
45
|
+
* Maps subflow output back into parent scope.
|
|
46
|
+
*
|
|
47
|
+
* **Array values are concatenated, not replaced.** `applyOutputMapping` uses
|
|
48
|
+
* `[...existing, ...value]` for arrays. Return only the **delta** (new items)
|
|
49
|
+
* for array keys, or the parent's existing array items will be duplicated.
|
|
50
|
+
* Scalar values are replaced normally.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* // WRONG — returns full array, parent concats → duplicates
|
|
55
|
+
* outputMapper: (sf) => ({ messages: sf.allMessages })
|
|
56
|
+
*
|
|
57
|
+
* // RIGHT — returns only new items (delta), concat appends correctly
|
|
58
|
+
* outputMapper: (sf) => ({ messages: sf.newMessages })
|
|
59
|
+
*
|
|
60
|
+
* // Scalars are fine — replaced, not concatenated
|
|
61
|
+
* outputMapper: (sf) => ({ status: sf.result, count: sf.total })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
44
64
|
outputMapper?: (subflowOutput: TSubflowOutput, parentScope: TParentScope) => Record<string, unknown>;
|
|
45
65
|
}
|
|
46
66
|
export interface SubflowResult {
|
|
@@ -54,6 +74,37 @@ export interface SubflowResult {
|
|
|
54
74
|
parentStageId: string;
|
|
55
75
|
pipelineStructure?: unknown;
|
|
56
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* SubflowTraverserFactory — Creates a FlowchartTraverser for subflow execution.
|
|
79
|
+
*
|
|
80
|
+
* Injected into SubflowExecutor to break the circular dependency:
|
|
81
|
+
* FlowchartTraverser → SubflowExecutor → FlowchartTraverser.
|
|
82
|
+
*
|
|
83
|
+
* The factory captures parent traverser config (stageMap, scopeFactory, narrative, etc.)
|
|
84
|
+
* in a closure. SubflowExecutor calls it with subflow-specific overrides (root, runtime, input).
|
|
85
|
+
* The returned traverser uses the SAME 7-phase algorithm as the top-level traverser,
|
|
86
|
+
* so deciders, selectors, loops, lazy subflows, and abort signals all work inside subflows.
|
|
87
|
+
*/
|
|
88
|
+
export type SubflowTraverserFactory<TOut = any, TScope = any> = (options: {
|
|
89
|
+
/** Root node of the subflow (with isSubflowRoot stripped). */
|
|
90
|
+
root: StageNode<TOut, TScope>;
|
|
91
|
+
/** Isolated execution runtime for the subflow. */
|
|
92
|
+
executionRuntime: IExecutionRuntime;
|
|
93
|
+
/** Mapped input from parent scope (becomes readOnlyContext for stages). */
|
|
94
|
+
readOnlyContext?: unknown;
|
|
95
|
+
/** Subflow identifier — used as branchPath for narrative context. */
|
|
96
|
+
subflowId?: string;
|
|
97
|
+
}) => SubflowTraverserHandle<TOut, TScope>;
|
|
98
|
+
/**
|
|
99
|
+
* Handle returned by SubflowTraverserFactory.
|
|
100
|
+
* Provides execute() + access to nested subflow results.
|
|
101
|
+
*/
|
|
102
|
+
export interface SubflowTraverserHandle<TOut = any, TScope = any> {
|
|
103
|
+
/** Execute the subflow's graph using the full 7-phase traversal algorithm. */
|
|
104
|
+
execute(): Promise<TraversalResult>;
|
|
105
|
+
/** Collect nested subflow results (from subflows mounted inside this subflow). */
|
|
106
|
+
getSubflowResults(): Map<string, SubflowResult>;
|
|
107
|
+
}
|
|
57
108
|
/**
|
|
58
109
|
* IExecutionRuntime — Interface for the runtime environment.
|
|
59
110
|
*
|
|
@@ -31,10 +31,12 @@ export declare class StageContext {
|
|
|
31
31
|
next?: StageContext;
|
|
32
32
|
children?: StageContext[];
|
|
33
33
|
debug: DiagnosticCollector;
|
|
34
|
-
/** Tracks user-level writes (pre-namespace) for the memory view. */
|
|
34
|
+
/** Tracks user-level writes (pre-namespace) for the memory view and onCommit. */
|
|
35
35
|
private _stageWrites;
|
|
36
36
|
/** Tracks user-level reads (pre-namespace) for the memory view. */
|
|
37
37
|
private _stageReads;
|
|
38
|
+
/** Observer called after commit() — used by ScopeFacade to fire Recorder.onCommit. */
|
|
39
|
+
private _commitObserver?;
|
|
38
40
|
constructor(runId: string, name: string, stageId: string, sharedMemory: SharedMemory, branchId?: string, eventLog?: EventLog, isDecider?: boolean);
|
|
39
41
|
/** Returns the SharedMemory instance (needed by scope layer). */
|
|
40
42
|
getSharedMemory(): SharedMemory;
|
|
@@ -45,18 +47,27 @@ export declare class StageContext {
|
|
|
45
47
|
patch(path: string[], key: string, value: unknown, shouldRedact?: boolean): void;
|
|
46
48
|
set(path: string[], key: string, value: unknown): void;
|
|
47
49
|
merge(path: string[], key: string, value: unknown): void;
|
|
48
|
-
setObject(path: string[], key: string, value: unknown, shouldRedact?: boolean, description?: string): void;
|
|
49
|
-
updateObject(path: string[], key: string, value: unknown, description?: string): void;
|
|
50
|
+
setObject(path: string[], key: string, value: unknown, shouldRedact?: boolean, description?: string, operationOverride?: 'set' | 'delete'): void;
|
|
51
|
+
updateObject(path: string[], key: string, value: unknown, description?: string, shouldRedact?: boolean): void;
|
|
50
52
|
setRoot(key: string, value: unknown): void;
|
|
51
53
|
setGlobal(key: string, value: unknown, description?: string): void;
|
|
52
54
|
updateGlobalContext(key: string, value: unknown): void;
|
|
53
55
|
appendToArray(path: string[], key: string, items: unknown[], description?: string): void;
|
|
54
56
|
mergeObject(path: string[], key: string, obj: Record<string, unknown>, description?: string): void;
|
|
55
57
|
getValue(path: string[], key?: string, description?: string): any;
|
|
58
|
+
/** Read state without tracking in _stageReads or paying structuredClone cost.
|
|
59
|
+
* Used by ScopeFacade.getValueSilent() for array proxy internal operations. */
|
|
60
|
+
getValueDirect(path: string[], key?: string): unknown;
|
|
56
61
|
getRoot(key: string): any;
|
|
57
62
|
getGlobal(key: string): any;
|
|
58
63
|
getScope(): Record<string, unknown>;
|
|
59
64
|
getRunId(): string;
|
|
65
|
+
/** Register an observer that fires after commit() applies patches.
|
|
66
|
+
* Used by ScopeFacade to dispatch Recorder.onCommit events. */
|
|
67
|
+
setCommitObserver(observer: (mutations: Record<string, {
|
|
68
|
+
value: unknown;
|
|
69
|
+
operation: 'set' | 'update' | 'delete';
|
|
70
|
+
}>) => void): void;
|
|
60
71
|
commit(): void;
|
|
61
72
|
createNext(path: string, stageName: string, stageId: string, isDecider?: boolean): StageContext;
|
|
62
73
|
createChild(runId: string, branchId: string, stageName: string, stageId: string, isDecider?: boolean): StageContext;
|
|
@@ -18,6 +18,8 @@ export interface ReactiveTarget {
|
|
|
18
18
|
getStateKeys?(): string[];
|
|
19
19
|
/** Check key existence without firing onRead. Used by has trap. */
|
|
20
20
|
hasKey?(key: string): boolean;
|
|
21
|
+
/** Read state without firing onRead. Used by array proxy getCurrent(). */
|
|
22
|
+
getValueSilent?(key?: string): unknown;
|
|
21
23
|
getArgs<T = Record<string, unknown>>(): T;
|
|
22
24
|
getEnv(): Readonly<ExecutionEnv>;
|
|
23
25
|
attachRecorder(recorder: Recorder): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompositeRecorder — fan-out a single recorder attachment to multiple child recorders.
|
|
3
|
+
*
|
|
4
|
+
* Implements both Recorder (scope data ops) and FlowRecorder (control flow events)
|
|
5
|
+
* so it works with both `executor.attachRecorder()` and `executor.attachFlowRecorder()`.
|
|
6
|
+
*
|
|
7
|
+
* The composite has a single ID for idempotent attach/detach. Child recorders
|
|
8
|
+
* keep their own IDs internally but are not individually visible to the executor.
|
|
9
|
+
*
|
|
10
|
+
* Domain libraries (e.g., agentfootprint) use this to bundle multiple recorders
|
|
11
|
+
* into a single preset — the consumer calls one function, gets full observability.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { CompositeRecorder, MetricRecorder, DebugRecorder } from 'footprintjs';
|
|
16
|
+
*
|
|
17
|
+
* // Bundle metrics + debug into a single recorder
|
|
18
|
+
* const observability = new CompositeRecorder('observability', [
|
|
19
|
+
* new MetricRecorder({ stageFilter: (name) => name === 'CallLLM' }),
|
|
20
|
+
* new DebugRecorder({ verbosity: 'minimal' }),
|
|
21
|
+
* ]);
|
|
22
|
+
*
|
|
23
|
+
* executor.attachRecorder(observability);
|
|
24
|
+
*
|
|
25
|
+
* // Access child recorders by type
|
|
26
|
+
* const metrics = observability.get(MetricRecorder);
|
|
27
|
+
* metrics?.getMetrics(); // timing data
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Domain library preset (e.g., agentfootprint)
|
|
33
|
+
* export function agentObservability(options?: AgentObservabilityOptions) {
|
|
34
|
+
* return new CompositeRecorder('agent-observability', [
|
|
35
|
+
* new MetricRecorder(options?.stageFilter ? { stageFilter: options.stageFilter } : undefined),
|
|
36
|
+
* new TokenRecorder(),
|
|
37
|
+
* new ToolUsageRecorder(),
|
|
38
|
+
* ]);
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* // Consumer
|
|
42
|
+
* executor.attachRecorder(agentObservability());
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowNextEvent, FlowRecorder, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent, FlowSubflowRegisteredEvent } from '../engine/narrative/types.js';
|
|
46
|
+
import type { CommitEvent, ErrorEvent, ReadEvent, Recorder, StageEvent, WriteEvent } from '../scope/types.js';
|
|
47
|
+
/** Snapshot format for composite recorders — wraps child snapshots. */
|
|
48
|
+
export interface CompositeSnapshot {
|
|
49
|
+
name: string;
|
|
50
|
+
data: {
|
|
51
|
+
children: Array<{
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
data: unknown;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export declare class CompositeRecorder implements Recorder, FlowRecorder {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
private readonly children;
|
|
61
|
+
constructor(id: string, children: Array<Recorder | FlowRecorder>);
|
|
62
|
+
/**
|
|
63
|
+
* Get a child recorder by class type.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const metrics = composite.get(MetricRecorder);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
get<T>(type: new (...args: any[]) => T): T | undefined;
|
|
71
|
+
/** Get all child recorders. */
|
|
72
|
+
getChildren(): ReadonlyArray<Recorder | FlowRecorder>;
|
|
73
|
+
onRead(event: ReadEvent): void;
|
|
74
|
+
onWrite(event: WriteEvent): void;
|
|
75
|
+
onCommit(event: CommitEvent): void;
|
|
76
|
+
onError(event: ErrorEvent | FlowErrorEvent): void;
|
|
77
|
+
onStageStart(event: StageEvent): void;
|
|
78
|
+
onStageEnd(event: StageEvent): void;
|
|
79
|
+
onStageExecuted(event: FlowStageEvent): void;
|
|
80
|
+
onNext(event: FlowNextEvent): void;
|
|
81
|
+
onDecision(event: FlowDecisionEvent): void;
|
|
82
|
+
onFork(event: FlowForkEvent): void;
|
|
83
|
+
onSelected(event: FlowSelectedEvent): void;
|
|
84
|
+
onSubflowEntry(event: FlowSubflowEvent): void;
|
|
85
|
+
onSubflowExit(event: FlowSubflowEvent): void;
|
|
86
|
+
onSubflowRegistered(event: FlowSubflowRegisteredEvent): void;
|
|
87
|
+
onLoop(event: FlowLoopEvent): void;
|
|
88
|
+
onBreak(event: FlowBreakEvent): void;
|
|
89
|
+
clear(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Snapshot merges all child snapshots into a single composite entry.
|
|
92
|
+
* Each child's snapshot is preserved with its own id/name/data.
|
|
93
|
+
*/
|
|
94
|
+
toSnapshot(): CompositeSnapshot;
|
|
95
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* const result = await executor.run({ input: data, env: { traceId: 'req-123' } });
|
|
18
18
|
*/
|
|
19
|
+
import type { CombinedNarrativeRecorderOptions } from '../engine/narrative/CombinedNarrativeRecorder.js';
|
|
19
20
|
import type { CombinedNarrativeEntry } from '../engine/narrative/narrativeTypes.js';
|
|
20
21
|
import type { ManifestEntry } from '../engine/narrative/recorders/ManifestFlowRecorder.js';
|
|
21
22
|
import type { FlowRecorder } from '../engine/narrative/types.js';
|
|
@@ -80,6 +81,7 @@ export interface FlowChartExecutorOptions<TScope = any> {
|
|
|
80
81
|
export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
81
82
|
private traverser;
|
|
82
83
|
private narrativeEnabled;
|
|
84
|
+
private narrativeOptions?;
|
|
83
85
|
private combinedRecorder;
|
|
84
86
|
private flowRecorders;
|
|
85
87
|
private scopeRecorders;
|
|
@@ -105,7 +107,7 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
105
107
|
*/
|
|
106
108
|
constructor(flowChart: FlowChart<TOut, TScope>, factoryOrOptions?: ScopeFactory<TScope> | FlowChartExecutorOptions<TScope>);
|
|
107
109
|
private createTraverser;
|
|
108
|
-
enableNarrative(): void;
|
|
110
|
+
enableNarrative(options?: CombinedNarrativeRecorderOptions): void;
|
|
109
111
|
/**
|
|
110
112
|
* Set a declarative redaction policy that applies to all stages.
|
|
111
113
|
* Must be called before run().
|
|
@@ -120,6 +122,28 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
120
122
|
* Attach a scope Recorder to observe data operations (reads, writes, commits).
|
|
121
123
|
* Automatically attached to every ScopeFacade created during traversal.
|
|
122
124
|
* Must be called before run().
|
|
125
|
+
*
|
|
126
|
+
* **Idempotent by ID:** If a recorder with the same `id` is already attached,
|
|
127
|
+
* it is replaced (not duplicated). This prevents double-counting when both
|
|
128
|
+
* a framework and the user attach the same recorder type.
|
|
129
|
+
*
|
|
130
|
+
* Built-in recorders use auto-increment IDs (`metrics-1`, `debug-1`, ...) by
|
|
131
|
+
* default, so multiple instances with different configs coexist. To override
|
|
132
|
+
* a framework-attached recorder, pass the same well-known ID.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* // Multiple recorders with different configs — each gets a unique ID
|
|
137
|
+
* executor.attachRecorder(new MetricRecorder());
|
|
138
|
+
* executor.attachRecorder(new DebugRecorder({ verbosity: 'minimal' }));
|
|
139
|
+
*
|
|
140
|
+
* // Override a framework-attached recorder by passing its well-known ID
|
|
141
|
+
* executor.attachRecorder(new MetricRecorder('metrics'));
|
|
142
|
+
*
|
|
143
|
+
* // Attaching twice with same ID replaces (no double-counting)
|
|
144
|
+
* executor.attachRecorder(new MetricRecorder('my-metrics'));
|
|
145
|
+
* executor.attachRecorder(new MetricRecorder('my-metrics')); // replaces previous
|
|
146
|
+
* ```
|
|
123
147
|
*/
|
|
124
148
|
attachRecorder(recorder: Recorder): void;
|
|
125
149
|
/** Detach all scope Recorders with the given ID. */
|
|
@@ -130,6 +154,8 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
130
154
|
* Attach a FlowRecorder to observe control flow events.
|
|
131
155
|
* Automatically enables narrative if not already enabled.
|
|
132
156
|
* Must be called before run() — recorders are passed to the traverser at creation time.
|
|
157
|
+
*
|
|
158
|
+
* **Idempotent by ID:** replaces existing recorder with same `id`.
|
|
133
159
|
*/
|
|
134
160
|
attachFlowRecorder(recorder: FlowRecorder): void;
|
|
135
161
|
/** Detach all FlowRecorders with the given ID. */
|
|
@@ -63,6 +63,9 @@ export declare class ScopeFacade {
|
|
|
63
63
|
notifyStageEnd(duration?: number): void;
|
|
64
64
|
/** @internal */
|
|
65
65
|
notifyCommit(mutations: CommitEvent['mutations']): void;
|
|
66
|
+
/** Called by StageContext.commit() observer. Converts tracked writes to CommitEvent format.
|
|
67
|
+
* Errors are caught to prevent recorder issues from aborting the traversal. */
|
|
68
|
+
private _onCommitFired;
|
|
66
69
|
addDebugInfo(key: string, value: unknown): void;
|
|
67
70
|
addDebugMessage(value: unknown): void;
|
|
68
71
|
addErrorInfo(key: string, value: unknown): void;
|
|
@@ -74,6 +77,14 @@ export declare class ScopeFacade {
|
|
|
74
77
|
* Contract: returns false for keys never set OR keys set to undefined.
|
|
75
78
|
* This matches deleteValue() semantics (sets to undefined = deleted). */
|
|
76
79
|
hasKey(key: string): boolean;
|
|
80
|
+
/** Read state without firing onRead. Used by array proxy getCurrent() to avoid
|
|
81
|
+
* phantom reads on internal array operations (.length, .has, iteration, etc.).
|
|
82
|
+
* The initial property access fires one tracked onRead via getValue(); subsequent
|
|
83
|
+
* internal array operations use this method to stay silent.
|
|
84
|
+
* NOTE: Like getValue(), returns the raw value to the caller. Redaction applies
|
|
85
|
+
* only to recorder dispatch — it does not filter the returned value. This matches
|
|
86
|
+
* the existing getValue() contract where user code always receives raw data. */
|
|
87
|
+
getValueSilent(key?: string): unknown;
|
|
77
88
|
getInitialValueFor(key: string): any;
|
|
78
89
|
getValue(key?: string): any;
|
|
79
90
|
setValue(key: string, value: unknown, shouldRedact?: boolean, description?: string): void;
|
|
@@ -16,7 +16,23 @@ export interface DebugRecorderOptions {
|
|
|
16
16
|
id?: string;
|
|
17
17
|
verbosity?: DebugVerbosity;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Each instance gets a unique auto-increment ID (`debug-1`, `debug-2`, ...),
|
|
21
|
+
* so multiple recorders with different verbosity coexist.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // Verbose debug for development
|
|
26
|
+
* executor.attachRecorder(new DebugRecorder({ verbosity: 'verbose' }));
|
|
27
|
+
*
|
|
28
|
+
* // Minimal debug for production (errors only)
|
|
29
|
+
* executor.attachRecorder(new DebugRecorder({ verbosity: 'minimal' }));
|
|
30
|
+
*
|
|
31
|
+
* // Both coexist — different auto IDs
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
19
34
|
export declare class DebugRecorder implements Recorder {
|
|
35
|
+
private static _counter;
|
|
20
36
|
readonly id: string;
|
|
21
37
|
private entries;
|
|
22
38
|
private verbosity;
|