footprintjs 4.12.2 → 4.13.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 +111 -0
- package/README.md +29 -1
- package/dist/esm/index.js +16 -3
- package/dist/esm/lib/engine/graph/StageNode.js +2 -3
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +29 -1
- package/dist/esm/lib/engine/handlers/types.js +1 -1
- package/dist/esm/lib/engine/index.js +1 -1
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +176 -41
- package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +8 -3
- 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/narrative/types.js +1 -1
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +60 -36
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/reactive/createTypedScope.js +6 -3
- package/dist/esm/lib/reactive/types.js +2 -1
- package/dist/esm/lib/recorder/CombinedRecorder.js +211 -0
- package/dist/esm/lib/recorder/EmitRecorder.js +62 -0
- package/dist/esm/lib/recorder/index.js +2 -1
- package/dist/esm/lib/runner/FlowChartExecutor.js +123 -1
- package/dist/esm/lib/scope/ScopeFacade.js +117 -1
- package/dist/esm/lib/scope/detectCircular.js +74 -5
- package/dist/esm/lib/scope/types.js +1 -1
- package/dist/esm/recorders.js +1 -1
- package/dist/index.js +50 -32
- package/dist/lib/engine/graph/StageNode.js +2 -3
- package/dist/lib/engine/handlers/SubflowExecutor.js +29 -1
- package/dist/lib/engine/handlers/types.js +1 -1
- package/dist/lib/engine/index.js +1 -1
- package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +176 -41
- package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +8 -3
- package/dist/lib/engine/narrative/index.js +1 -1
- package/dist/lib/engine/narrative/narrativeTypes.js +1 -1
- package/dist/lib/engine/narrative/types.js +1 -1
- package/dist/lib/engine/traversal/FlowchartTraverser.js +60 -36
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/reactive/createTypedScope.js +6 -3
- package/dist/lib/reactive/types.js +2 -1
- package/dist/lib/recorder/CombinedRecorder.js +218 -0
- package/dist/lib/recorder/EmitRecorder.js +63 -0
- package/dist/lib/recorder/index.js +7 -2
- package/dist/lib/runner/FlowChartExecutor.js +123 -1
- package/dist/lib/scope/ScopeFacade.js +117 -1
- package/dist/lib/scope/detectCircular.js +74 -5
- package/dist/lib/scope/types.js +1 -1
- package/dist/recorders.js +1 -1
- package/dist/types/index.d.ts +36 -2
- package/dist/types/lib/engine/graph/StageNode.d.ts +3 -7
- package/dist/types/lib/engine/handlers/SubflowExecutor.d.ts +2 -3
- package/dist/types/lib/engine/handlers/types.d.ts +19 -3
- package/dist/types/lib/engine/index.d.ts +1 -1
- package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +38 -17
- package/dist/types/lib/engine/narrative/FlowRecorderDispatcher.d.ts +1 -1
- package/dist/types/lib/engine/narrative/index.d.ts +1 -1
- package/dist/types/lib/engine/narrative/narrativeTypes.d.ts +70 -6
- package/dist/types/lib/engine/narrative/types.d.ts +24 -2
- package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +18 -0
- package/dist/types/lib/engine/types.d.ts +66 -0
- package/dist/types/lib/reactive/types.d.ts +61 -3
- package/dist/types/lib/recorder/CombinedRecorder.d.ts +173 -0
- package/dist/types/lib/recorder/EmitRecorder.d.ts +135 -0
- package/dist/types/lib/recorder/index.d.ts +3 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +85 -0
- package/dist/types/lib/scope/ScopeFacade.d.ts +40 -0
- package/dist/types/lib/scope/detectCircular.d.ts +30 -3
- package/dist/types/lib/scope/types.d.ts +21 -0
- package/dist/types/recorders.d.ts +3 -2
- package/package.json +1 -2
package/dist/types/index.d.ts
CHANGED
|
@@ -45,10 +45,33 @@ export { MetricRecorder } from './lib/scope/index.js';
|
|
|
45
45
|
export { DebugRecorder } from './lib/scope/index.js';
|
|
46
46
|
/** @category Observe — Operation */
|
|
47
47
|
export { RecorderOperation } from './lib/recorder/index.js';
|
|
48
|
+
/** @category Observe — Combined (both data-flow and control-flow) */
|
|
49
|
+
export type { CombinedRecorder } from './lib/recorder/index.js';
|
|
50
|
+
/** @category Observe — Combined (both data-flow and control-flow) */
|
|
51
|
+
export { hasEmitRecorderMethods, hasFlowRecorderMethods, hasRecorderMethods, isFlowEvent, } from './lib/recorder/index.js';
|
|
52
|
+
/**
|
|
53
|
+
* @category Observe — Emit (user-authored structured events)
|
|
54
|
+
*
|
|
55
|
+
* Third observer channel (alongside `Recorder` and `FlowRecorder`). Consumer
|
|
56
|
+
* code calls `scope.$emit(name, payload)` from inside a stage; every attached
|
|
57
|
+
* `EmitRecorder.onEmit(event)` fires synchronously with stage-context
|
|
58
|
+
* enrichment. Pass-through — no buffering, zero allocation when no recorder
|
|
59
|
+
* is attached.
|
|
60
|
+
*/
|
|
61
|
+
export type { EmitEvent, EmitRecorder } from './lib/recorder/index.js';
|
|
48
62
|
/** @category Observe — Flow */
|
|
49
63
|
export type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowNextEvent, FlowRecorder, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent, FlowSubflowRegisteredEvent, TraversalContext, } from './lib/engine/index.js';
|
|
50
64
|
/** @category Observe — Flow */
|
|
51
65
|
export type { CombinedNarrativeEntry } from './lib/engine/index.js';
|
|
66
|
+
/**
|
|
67
|
+
* @category Observe — Flow
|
|
68
|
+
*
|
|
69
|
+
* `NarrativeFormatter` — pluggable formatter that converts event context
|
|
70
|
+
* objects into the text lines of the narrative. Prefer this name in new
|
|
71
|
+
* code; `NarrativeRenderer` is a deprecated alias that will be removed in
|
|
72
|
+
* the next major release.
|
|
73
|
+
*/
|
|
74
|
+
export type { NarrativeFormatter, NarrativeRenderer } from './lib/engine/index.js';
|
|
52
75
|
/** @category Observe — Flow */
|
|
53
76
|
export { NarrativeFlowRecorder } from './lib/engine/index.js';
|
|
54
77
|
/** @category Observe — Flow */
|
|
@@ -98,7 +121,18 @@ export { InputValidationError, validateAgainstSchema, validateOrThrow } from './
|
|
|
98
121
|
export type { StructuredErrorInfo } from './lib/engine/index.js';
|
|
99
122
|
/** @category Error Utilities */
|
|
100
123
|
export { extractErrorInfo, formatErrorInfo } from './lib/engine/index.js';
|
|
101
|
-
/**
|
|
102
|
-
|
|
124
|
+
/**
|
|
125
|
+
* @category Dev Tools
|
|
126
|
+
*
|
|
127
|
+
* Global dev-mode flag. Call `enableDevMode()` at application startup to
|
|
128
|
+
* turn on developer-only diagnostics across the library — circular-reference
|
|
129
|
+
* detection in scope writes, warnings when a recorder has no observer
|
|
130
|
+
* methods, suspicious-predicate warnings in decide/select, structural
|
|
131
|
+
* checks in `getSubtreeSnapshot`, and any future dev-only diagnostic.
|
|
132
|
+
*
|
|
133
|
+
* Production leaves it OFF by default (zero overhead). See the JSDoc on
|
|
134
|
+
* `enableDevMode` for the full list and usage example.
|
|
135
|
+
*/
|
|
136
|
+
export { disableDevMode, enableDevMode, isDevMode } from './lib/scope/detectCircular.js';
|
|
103
137
|
/** @category Dev Tools */
|
|
104
138
|
export { defineScopeFromZod } from './lib/scope/index.js';
|
|
@@ -74,12 +74,9 @@ export type StageNode<TOut = any, TScope = any> = {
|
|
|
74
74
|
* to distinguish runtime graph field from the JSON-safe spec field).
|
|
75
75
|
*/
|
|
76
76
|
isLoopRef?: boolean;
|
|
77
|
-
/** Inline subflow definition for dynamic subflow attachment.
|
|
78
|
-
* When `root` is omitted, the subflow is structural-only:
|
|
79
|
-
* the engine attaches `buildTimeStructure` for visualization
|
|
80
|
-
* without executing any subflow stages (pre-executed subflow pattern). */
|
|
77
|
+
/** Inline subflow definition for dynamic subflow attachment. */
|
|
81
78
|
subflowDef?: {
|
|
82
|
-
root
|
|
79
|
+
root: StageNode;
|
|
83
80
|
stageMap?: Map<string, StageFunction<TOut, TScope>>;
|
|
84
81
|
buildTimeStructure?: unknown;
|
|
85
82
|
subflows?: Record<string, {
|
|
@@ -104,8 +101,7 @@ export type StageNode<TOut = any, TScope = any> = {
|
|
|
104
101
|
* Uses duck-typing: must have `name` (string) AND at least one continuation
|
|
105
102
|
* property (non-empty children, next, nextNodeSelector, or isSubflowRoot).
|
|
106
103
|
*
|
|
107
|
-
* `isSubflowRoot` counts as continuation because subflow execution
|
|
108
|
-
* structural annotation for pre-executed subflows) is a form of continuation.
|
|
104
|
+
* `isSubflowRoot` counts as continuation because subflow execution is a form of continuation.
|
|
109
105
|
*
|
|
110
106
|
* Safely handles proxy objects (e.g., Zod scope) that may throw on property access.
|
|
111
107
|
*/
|
|
@@ -16,6 +16,7 @@ import type { StageContext } from '../../memory/StageContext.js';
|
|
|
16
16
|
import type { StageNode } from '../graph/StageNode.js';
|
|
17
17
|
import type { TraversalContext } from '../narrative/types.js';
|
|
18
18
|
import type { HandlerDeps, SubflowResult, SubflowTraverserFactory } from '../types.js';
|
|
19
|
+
import type { BreakFlag } from './types.js';
|
|
19
20
|
export declare class SubflowExecutor<TOut = any, TScope = any> {
|
|
20
21
|
private deps;
|
|
21
22
|
private traverserFactory;
|
|
@@ -29,7 +30,5 @@ export declare class SubflowExecutor<TOut = any, TScope = any> {
|
|
|
29
30
|
* 4. Applies output mapping to write results back to parent scope
|
|
30
31
|
* 5. Stores execution data for debugging/visualization
|
|
31
32
|
*/
|
|
32
|
-
executeSubflow(node: StageNode<TOut, TScope>, parentContext: StageContext, breakFlag:
|
|
33
|
-
shouldBreak: boolean;
|
|
34
|
-
}, branchPath: string | undefined, subflowResultsMap: Map<string, SubflowResult>, parentTraversalContext?: TraversalContext): Promise<any>;
|
|
33
|
+
executeSubflow(node: StageNode<TOut, TScope>, parentContext: StageContext, breakFlag: BreakFlag, branchPath: string | undefined, subflowResultsMap: Map<string, SubflowResult>, parentTraversalContext?: TraversalContext): Promise<any>;
|
|
35
34
|
}
|
|
@@ -8,10 +8,26 @@
|
|
|
8
8
|
import type { StageContext } from '../../memory/StageContext.js';
|
|
9
9
|
import type { StageNode } from '../graph/StageNode.js';
|
|
10
10
|
import type { StageFunction } from '../types.js';
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Traverser break flag — mutable struct passed through the traversal recursion.
|
|
13
|
+
*
|
|
14
|
+
* `shouldBreak` flips when any stage calls `scope.$break()`. The traverser
|
|
15
|
+
* checks this before scheduling the next node; when set, the stack unwinds
|
|
16
|
+
* cleanly without running subsequent stages.
|
|
17
|
+
*
|
|
18
|
+
* `reason` is the optional string argument passed to `$break(reason)`. It
|
|
19
|
+
* travels alongside `shouldBreak` so downstream code (FlowRecorder onBreak
|
|
20
|
+
* events, subflow-propagation to parent scope) can surface the reason.
|
|
21
|
+
*
|
|
22
|
+
* Adding fields here is backward compatible — existing callers only read
|
|
23
|
+
* `shouldBreak`, and `reason` is optional.
|
|
24
|
+
*/
|
|
25
|
+
export interface BreakFlag {
|
|
13
26
|
shouldBreak: boolean;
|
|
14
|
-
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Recursive node execution — avoids circular dep with FlowchartTraverser. */
|
|
30
|
+
export type ExecuteNodeFn<TOut = any, TScope = any> = (node: StageNode<TOut, TScope>, context: StageContext, breakFlag: BreakFlag, branchPath?: string) => Promise<any>;
|
|
15
31
|
/** Run a stage function with commit + extractor. */
|
|
16
32
|
export type RunStageFn<TOut = any, TScope = any> = (node: StageNode<TOut, TScope>, stageFunc: StageFunction<TOut, TScope>, context: StageContext, breakFn: () => void) => Promise<TOut>;
|
|
17
33
|
/** Call the traversal extractor after stage execution. */
|
|
@@ -9,7 +9,7 @@ export { FlowchartTraverser } from './traversal/FlowchartTraverser.js';
|
|
|
9
9
|
export { isStageNodeReturn } from './graph/StageNode.js';
|
|
10
10
|
export * from './types.js';
|
|
11
11
|
export * from './handlers/index.js';
|
|
12
|
-
export type { CombinedNarrativeEntry, CombinedNarrativeOptions } from './narrative/narrativeTypes.js';
|
|
12
|
+
export type { CombinedNarrativeEntry, CombinedNarrativeOptions, EmitRenderContext, NarrativeFormatter, NarrativeRenderer, } from './narrative/narrativeTypes.js';
|
|
13
13
|
export { NullControlFlowNarrativeGenerator } from './narrative/NullControlFlowNarrativeGenerator.js';
|
|
14
14
|
export type { IControlFlowNarrative } from './narrative/types.js';
|
|
15
15
|
export { FlowRecorderDispatcher } from './narrative/FlowRecorderDispatcher.js';
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* CombinedNarrativeRecorder — Inline narrative builder that merges flow + data during traversal.
|
|
3
3
|
*
|
|
4
4
|
* Extends SequenceRecorder<CombinedNarrativeEntry> for dual-indexed storage (ordered sequence
|
|
5
|
-
* + O(1) per-step lookup by runtimeStageId). Implements
|
|
6
|
-
*
|
|
5
|
+
* + O(1) per-step lookup by runtimeStageId). Implements `CombinedRecorder` — the library's
|
|
6
|
+
* first-class abstraction for observers that span both data-flow and control-flow streams.
|
|
7
7
|
*
|
|
8
8
|
* Event ordering guarantees this works:
|
|
9
9
|
* 1. Scope events (onRead, onWrite) fire DURING stage execution
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
* So we buffer scope ops per-stage, then when the flow event arrives,
|
|
14
14
|
* emit the stage entry + flush the buffered ops in one pass.
|
|
15
15
|
*/
|
|
16
|
+
import type { CombinedRecorder } from '../../recorder/CombinedRecorder.js';
|
|
17
|
+
import type { EmitEvent } from '../../recorder/EmitRecorder.js';
|
|
16
18
|
import { SequenceRecorder } from '../../recorder/SequenceRecorder.js';
|
|
17
|
-
import type { ReadEvent,
|
|
19
|
+
import type { ErrorEvent, PauseEvent, ReadEvent, ResumeEvent, WriteEvent } from '../../scope/types.js';
|
|
18
20
|
import type { CombinedNarrativeEntry, NarrativeRenderer } from './narrativeTypes.js';
|
|
19
|
-
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowPauseEvent,
|
|
21
|
+
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowPauseEvent, FlowResumeEvent, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent } from './types.js';
|
|
20
22
|
export interface CombinedNarrativeRecorderOptions {
|
|
21
23
|
includeStepNumbers?: boolean;
|
|
22
24
|
includeValues?: boolean;
|
|
@@ -28,7 +30,19 @@ export interface CombinedNarrativeRecorderOptions {
|
|
|
28
30
|
* fall back to the default English renderer. See NarrativeRenderer docs. */
|
|
29
31
|
renderer?: NarrativeRenderer;
|
|
30
32
|
}
|
|
31
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Implements `CombinedRecorder` — the library's first-class abstraction for
|
|
35
|
+
* observers that span both data-flow (`Recorder`) and control-flow
|
|
36
|
+
* (`FlowRecorder`) streams. One `id`, routed to both channels via
|
|
37
|
+
* `executor.attachCombinedRecorder(...)` (or equivalently via
|
|
38
|
+
* `executor.enableNarrative(...)` which auto-creates an instance).
|
|
39
|
+
*
|
|
40
|
+
* For shared-method-name events (`onError`, `onPause`, `onResume`) the
|
|
41
|
+
* handler accepts the union payload type; we discriminate via `isFlowEvent`.
|
|
42
|
+
* Scope variants of these events are deliberately ignored here — the
|
|
43
|
+
* narrative only surfaces control-flow lifecycle events.
|
|
44
|
+
*/
|
|
45
|
+
export declare class CombinedNarrativeRecorder extends SequenceRecorder<CombinedNarrativeEntry> implements CombinedRecorder {
|
|
32
46
|
readonly id: string;
|
|
33
47
|
/**
|
|
34
48
|
* Pending scope ops keyed by runtimeStageId. Flushed in onStageExecuted/onDecision.
|
|
@@ -62,18 +76,25 @@ export declare class CombinedNarrativeRecorder extends SequenceRecorder<Combined
|
|
|
62
76
|
onSubflowExit(event: FlowSubflowEvent): void;
|
|
63
77
|
onLoop(event: FlowLoopEvent): void;
|
|
64
78
|
onBreak(event: FlowBreakEvent): void;
|
|
65
|
-
onPause(event:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
onPause(event: PauseEvent | FlowPauseEvent): void;
|
|
80
|
+
onResume(event: ResumeEvent | FlowResumeEvent): void;
|
|
81
|
+
onError(event: ErrorEvent | FlowErrorEvent): void;
|
|
82
|
+
/**
|
|
83
|
+
* Receive a consumer-emitted event from `scope.$emit(name, payload)`.
|
|
84
|
+
*
|
|
85
|
+
* Buffered alongside `onRead`/`onWrite` per-stage so that the final
|
|
86
|
+
* narrative preserves ordering:
|
|
87
|
+
*
|
|
88
|
+
* 1. stage header (emitted by `onStageExecuted` / `onDecision`)
|
|
89
|
+
* 2. buffered ops for that stage — in call order — flushed right after
|
|
90
|
+
*
|
|
91
|
+
* Without buffering, emit events would fire BEFORE the stage header
|
|
92
|
+
* (which only lands at `onStageExecuted`), producing out-of-order
|
|
93
|
+
* narrative entries. Flush happens in `flushOps` which routes `emit`-
|
|
94
|
+
* typed buffered ops through `renderEmit` instead of `renderOp`.
|
|
95
|
+
*/
|
|
96
|
+
onEmit(event: EmitEvent): void;
|
|
97
|
+
private defaultRenderEmit;
|
|
77
98
|
/** Returns formatted narrative lines (same output as CombinedNarrativeBuilder.build). */
|
|
78
99
|
getNarrative(indent?: string): string[];
|
|
79
100
|
/**
|
|
@@ -30,7 +30,7 @@ export declare class FlowRecorderDispatcher implements IControlFlowNarrative {
|
|
|
30
30
|
onSubflowExit(subflowName: string, subflowId?: string, traversalContext?: TraversalContext, outputState?: Record<string, unknown>): void;
|
|
31
31
|
onSubflowRegistered(subflowId: string, name: string, description?: string, specStructure?: unknown): void;
|
|
32
32
|
onLoop(targetStage: string, iteration: number, description?: string, traversalContext?: TraversalContext): void;
|
|
33
|
-
onBreak(stageName: string, traversalContext?: TraversalContext): void;
|
|
33
|
+
onBreak(stageName: string, traversalContext?: TraversalContext, reason?: string, propagatedFromSubflow?: string): void;
|
|
34
34
|
onError(stageName: string, errorMessage: string, error: unknown, traversalContext?: TraversalContext): void;
|
|
35
35
|
onPause(stageName: string, stageId: string, pauseData: unknown, subflowPath: readonly string[], traversalContext?: TraversalContext): void;
|
|
36
36
|
onResume(stageName: string, stageId: string, hasInput: boolean, traversalContext?: TraversalContext): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { CombinedNarrativeRecorderOptions } from './CombinedNarrativeRecorder.js';
|
|
2
2
|
export { CombinedNarrativeRecorder } from './CombinedNarrativeRecorder.js';
|
|
3
|
-
export type { BreakRenderContext, CombinedNarrativeEntry, CombinedNarrativeOptions, DecisionRenderContext, ErrorRenderContext, ForkRenderContext, LoopRenderContext, NarrativeRenderer, OpRenderContext, SelectedRenderContext, StageRenderContext, SubflowRenderContext, } from './narrativeTypes.js';
|
|
3
|
+
export type { BreakRenderContext, CombinedNarrativeEntry, CombinedNarrativeOptions, DecisionRenderContext, EmitRenderContext, ErrorRenderContext, ForkRenderContext, LoopRenderContext, NarrativeFormatter, 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';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Use CombinedNarrativeRecorder (auto-attached by setEnableNarrative()) instead.
|
|
7
7
|
*/
|
|
8
8
|
export interface CombinedNarrativeEntry {
|
|
9
|
-
type: 'stage' | 'step' | 'condition' | 'fork' | 'selector' | 'subflow' | 'loop' | 'break' | 'error' | 'pause' | 'resume';
|
|
9
|
+
type: 'stage' | 'step' | 'condition' | 'fork' | 'selector' | 'subflow' | 'loop' | 'break' | 'error' | 'pause' | 'resume' | 'emit';
|
|
10
10
|
text: string;
|
|
11
11
|
depth: number;
|
|
12
12
|
stageName?: string;
|
|
@@ -102,11 +102,42 @@ export interface ErrorRenderContext {
|
|
|
102
102
|
validationIssues?: string;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
105
|
+
* Context passed to `NarrativeFormatter.renderEmit` — fires for every
|
|
106
|
+
* consumer-emitted event (`scope.$emit(name, payload)`). Carries the full
|
|
107
|
+
* `EmitEvent` shape so formatters can render name + payload with full
|
|
108
|
+
* context.
|
|
109
|
+
*/
|
|
110
|
+
export interface EmitRenderContext {
|
|
111
|
+
name: string;
|
|
112
|
+
payload: unknown;
|
|
113
|
+
stageName: string;
|
|
114
|
+
runtimeStageId: string;
|
|
115
|
+
subflowPath: readonly string[];
|
|
116
|
+
pipelineId: string;
|
|
117
|
+
timestamp: number;
|
|
118
|
+
/** Summary string — the library's default truncated payload preview. */
|
|
119
|
+
payloadSummary: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Pluggable formatter for narrative output. Turns structured event context
|
|
123
|
+
* objects into the text lines that make up the narrative array.
|
|
124
|
+
*
|
|
125
|
+
* ## Why "Formatter", not "Renderer"
|
|
126
|
+
*
|
|
127
|
+
* In web terminology "renderer" usually means "turn data into a visible UI"
|
|
128
|
+
* (pixels, DOM, HTML). This interface does something smaller: it converts
|
|
129
|
+
* event context into a text string. That is more accurately called a
|
|
130
|
+
* *formatter* — hence the new name. The legacy alias `NarrativeRenderer`
|
|
131
|
+
* is preserved for backward compatibility and marked `@deprecated`; it will
|
|
132
|
+
* be removed in the next major release.
|
|
133
|
+
*
|
|
134
|
+
* ## Behaviour
|
|
106
135
|
*
|
|
107
136
|
* Each method is optional — unimplemented methods fall back to the default
|
|
108
|
-
* English
|
|
137
|
+
* English formatter. Return `null` from `renderOp` to exclude that entry
|
|
138
|
+
* from the narrative entirely.
|
|
109
139
|
*
|
|
140
|
+
* @example
|
|
110
141
|
* ```typescript
|
|
111
142
|
* const rec = narrative({
|
|
112
143
|
* renderer: {
|
|
@@ -118,10 +149,21 @@ export interface ErrorRenderContext {
|
|
|
118
149
|
* });
|
|
119
150
|
* ```
|
|
120
151
|
*/
|
|
121
|
-
export interface
|
|
152
|
+
export interface NarrativeFormatter {
|
|
122
153
|
renderStage?(ctx: StageRenderContext): string;
|
|
123
|
-
/**
|
|
124
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Format an op (scope read/write, or subflow-input key) into a narrative line.
|
|
156
|
+
*
|
|
157
|
+
* - `string` → use as the narrative line
|
|
158
|
+
* - `null` → deliberately exclude this entry from the narrative
|
|
159
|
+
* - `undefined` → this formatter does not handle this op; fall back to
|
|
160
|
+
* the library's default template
|
|
161
|
+
*
|
|
162
|
+
* The `undefined` return lets a domain-aware formatter handle only the
|
|
163
|
+
* keys it knows about and leave the rest to the library default — without
|
|
164
|
+
* having to re-implement the whole default inline.
|
|
165
|
+
*/
|
|
166
|
+
renderOp?(ctx: OpRenderContext): string | null | undefined;
|
|
125
167
|
renderDecision?(ctx: DecisionRenderContext): string;
|
|
126
168
|
renderFork?(ctx: ForkRenderContext): string;
|
|
127
169
|
renderSelected?(ctx: SelectedRenderContext): string;
|
|
@@ -129,4 +171,26 @@ export interface NarrativeRenderer {
|
|
|
129
171
|
renderLoop?(ctx: LoopRenderContext): string;
|
|
130
172
|
renderBreak?(ctx: BreakRenderContext): string;
|
|
131
173
|
renderError?(ctx: ErrorRenderContext): string;
|
|
174
|
+
/**
|
|
175
|
+
* Format a consumer-emitted event (from `scope.$emit(name, payload)`)
|
|
176
|
+
* into a narrative line.
|
|
177
|
+
*
|
|
178
|
+
* - `string` → use as the narrative line
|
|
179
|
+
* - `null` → deliberately exclude this entry
|
|
180
|
+
* - `undefined` → this formatter does not handle this emit; fall back
|
|
181
|
+
* to the library's default template
|
|
182
|
+
*
|
|
183
|
+
* Typical pattern: switch on `ctx.name` (or a prefix of it) and return a
|
|
184
|
+
* domain-specific text line for the event types you care about; return
|
|
185
|
+
* `undefined` for everything else so the default template handles it.
|
|
186
|
+
*/
|
|
187
|
+
renderEmit?(ctx: EmitRenderContext): string | null | undefined;
|
|
132
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* @deprecated Renamed to `NarrativeFormatter` for clarity — this interface
|
|
191
|
+
* formats event context into text lines, not "render to UI". Legacy alias
|
|
192
|
+
* kept for backward compatibility; will be removed in the next major
|
|
193
|
+
* release. Migrate by replacing `NarrativeRenderer` with
|
|
194
|
+
* `NarrativeFormatter` at imports — no behavioural change.
|
|
195
|
+
*/
|
|
196
|
+
export type NarrativeRenderer = NarrativeFormatter;
|
|
@@ -32,8 +32,16 @@ export interface IControlFlowNarrative {
|
|
|
32
32
|
onSubflowRegistered(subflowId: string, name: string, description?: string, specStructure?: unknown): void;
|
|
33
33
|
/** Called on loop iteration (back-edge traversal). */
|
|
34
34
|
onLoop(targetStage: string, iteration: number, description?: string, traversalContext?: TraversalContext): void;
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Called when a stage triggers break (early termination).
|
|
37
|
+
*
|
|
38
|
+
* @param reason - Optional string passed to `scope.$break(reason)`.
|
|
39
|
+
* @param propagatedFromSubflow - When set, this break was raised on the
|
|
40
|
+
* parent because an inner subflow (this id) broke with `propagateBreak`
|
|
41
|
+
* enabled. Used by recorders to distinguish originating vs propagated
|
|
42
|
+
* breaks and render them accordingly.
|
|
43
|
+
*/
|
|
44
|
+
onBreak(stageName: string, traversalContext?: TraversalContext, reason?: string, propagatedFromSubflow?: string): void;
|
|
37
45
|
/** Called when a stage throws an error. Raw error is extracted into structured details. */
|
|
38
46
|
onError(stageName: string, errorMessage: string, error: unknown, traversalContext?: TraversalContext): void;
|
|
39
47
|
/** Called when a pausable stage pauses execution. */
|
|
@@ -146,6 +154,20 @@ export interface FlowLoopEvent {
|
|
|
146
154
|
export interface FlowBreakEvent {
|
|
147
155
|
stageName: string;
|
|
148
156
|
traversalContext?: TraversalContext;
|
|
157
|
+
/**
|
|
158
|
+
* Optional free-form reason supplied by `scope.$break(reason)`. Absent
|
|
159
|
+
* when the stage invoked `$break()` without an argument. Propagates when
|
|
160
|
+
* a subflow is mounted with `propagateBreak: true` — the outer break
|
|
161
|
+
* event carries the inner break's reason too.
|
|
162
|
+
*/
|
|
163
|
+
reason?: string;
|
|
164
|
+
/**
|
|
165
|
+
* When true, this break event was raised on the PARENT because an inner
|
|
166
|
+
* subflow's break propagated up (via `SubflowMountOptions.propagateBreak`).
|
|
167
|
+
* The originating inner break fires its own `onBreak` event separately
|
|
168
|
+
* — this flag lets recorders distinguish the two.
|
|
169
|
+
*/
|
|
170
|
+
propagatedFromSubflow?: string;
|
|
149
171
|
}
|
|
150
172
|
/** Event passed to FlowRecorder.onError. */
|
|
151
173
|
export interface FlowErrorEvent {
|
|
@@ -135,7 +135,25 @@ export declare class FlowchartTraverser<TOut = any, TScope = any> {
|
|
|
135
135
|
*/
|
|
136
136
|
private createSubflowTraverserFactory;
|
|
137
137
|
private createDeps;
|
|
138
|
+
/**
|
|
139
|
+
* Holds the top-level break flag for the duration of `execute()`. Kept as
|
|
140
|
+
* a field (not a local) so `getBreakState()` can surface the final state
|
|
141
|
+
* for callers like `SubflowExecutor` that implement `propagateBreak`.
|
|
142
|
+
*/
|
|
143
|
+
private _topBreakFlag;
|
|
138
144
|
execute(branchPath?: string): Promise<TraversalResult>;
|
|
145
|
+
/**
|
|
146
|
+
* Break state captured at the top-level of the most recent `execute()`.
|
|
147
|
+
* `shouldBreak` is true when a stage called `scope.$break(reason)`; the
|
|
148
|
+
* optional `reason` carries the string passed to `$break`.
|
|
149
|
+
*
|
|
150
|
+
* Used by `SubflowExecutor` to propagate an inner subflow's break up to
|
|
151
|
+
* the parent traverser when the mount sets `propagateBreak: true`.
|
|
152
|
+
*/
|
|
153
|
+
getBreakState(): {
|
|
154
|
+
shouldBreak: boolean;
|
|
155
|
+
reason?: string;
|
|
156
|
+
};
|
|
139
157
|
getRuntimeStructure(): SerializedPipelineStructure | undefined;
|
|
140
158
|
getSnapshot(): {
|
|
141
159
|
sharedState: Record<string, unknown>;
|
|
@@ -89,6 +89,57 @@ export interface SubflowMountOptions<TParentScope = any, TSubflowInput = any, TS
|
|
|
89
89
|
* @default ArrayMergeMode.Concat
|
|
90
90
|
*/
|
|
91
91
|
arrayMerge?: ArrayMergeMode;
|
|
92
|
+
/**
|
|
93
|
+
* When `true`, an inner `scope.$break(reason)` call inside this subflow
|
|
94
|
+
* propagates up to the parent — i.e., the parent's `breakFlag` is set
|
|
95
|
+
* after the subflow exits, terminating the parent's outer loop too.
|
|
96
|
+
*
|
|
97
|
+
* **Default: `false`** (current behaviour — inner break stops only the
|
|
98
|
+
* subflow; parent continues).
|
|
99
|
+
*
|
|
100
|
+
* ## When to use
|
|
101
|
+
*
|
|
102
|
+
* Set `propagateBreak: true` on subflow mounts that represent
|
|
103
|
+
* **terminal** branches — "if this subflow fires, the outer loop is
|
|
104
|
+
* done". Examples:
|
|
105
|
+
*
|
|
106
|
+
* - A human-review runner that takes over from an agent's tool-calling
|
|
107
|
+
* loop and produces the final response.
|
|
108
|
+
* - A safety-gate subflow that halts the outer workflow when a policy
|
|
109
|
+
* violation is detected.
|
|
110
|
+
* - An error-recovery subflow that restores state and then terminates.
|
|
111
|
+
*
|
|
112
|
+
* ## Semantics
|
|
113
|
+
*
|
|
114
|
+
* 1. Inside the subflow, a stage calls `scope.$break(reason)`.
|
|
115
|
+
* 2. The subflow's own execution stops (normal `$break` behaviour).
|
|
116
|
+
* 3. `SubflowExecutor` inspects the subflow's exit state. If
|
|
117
|
+
* `propagateBreak === true` AND the inner break fired, it forwards
|
|
118
|
+
* the break (and its reason) to the parent's `breakFlag`.
|
|
119
|
+
* 4. The parent traverser sees `shouldBreak` on its next step and exits.
|
|
120
|
+
* 5. A `FlowRecorder.onBreak` event fires at the parent-mount level with
|
|
121
|
+
* `propagatedFromSubflow` = the subflow's id and the inner reason.
|
|
122
|
+
*
|
|
123
|
+
* ## Parallel/fan-out
|
|
124
|
+
*
|
|
125
|
+
* Follows the existing library rule in `ChildrenExecutor`: the parent
|
|
126
|
+
* breaks only when **every** child of a fork broke. A single
|
|
127
|
+
* `propagateBreak: true` subflow contributing its break to the count
|
|
128
|
+
* does not on its own terminate the parent fan-out.
|
|
129
|
+
*
|
|
130
|
+
* ## outputMapper still runs
|
|
131
|
+
*
|
|
132
|
+
* The subflow's `outputMapper` (if supplied) ALWAYS runs before the
|
|
133
|
+
* break propagates, so the subflow's partial state is still written to
|
|
134
|
+
* the parent scope. This is intentional — the typical use case is an
|
|
135
|
+
* "escalation" subflow whose output IS the final answer that needs to
|
|
136
|
+
* land in the parent scope before the outer loop terminates. If you
|
|
137
|
+
* want to suppress output mapping on break, check the break state
|
|
138
|
+
* inside your `outputMapper` and return `{}` early.
|
|
139
|
+
*
|
|
140
|
+
* @default false
|
|
141
|
+
*/
|
|
142
|
+
propagateBreak?: boolean;
|
|
92
143
|
}
|
|
93
144
|
export interface SubflowResult {
|
|
94
145
|
subflowId: string;
|
|
@@ -131,6 +182,21 @@ export interface SubflowTraverserHandle<TOut = any, TScope = any> {
|
|
|
131
182
|
execute(): Promise<TraversalResult>;
|
|
132
183
|
/** Collect nested subflow results (from subflows mounted inside this subflow). */
|
|
133
184
|
getSubflowResults(): Map<string, SubflowResult>;
|
|
185
|
+
/**
|
|
186
|
+
* Final break state of the subflow after `execute()` returns.
|
|
187
|
+
*
|
|
188
|
+
* - `shouldBreak: true` → a stage inside the subflow called
|
|
189
|
+
* `scope.$break(reason)`, stopping the subflow's own traversal.
|
|
190
|
+
* - `reason` → the optional string passed to `$break`.
|
|
191
|
+
*
|
|
192
|
+
* Used by `SubflowExecutor` to implement `SubflowMountOptions.propagateBreak`:
|
|
193
|
+
* if the mount opts in AND the subflow broke, the parent's break flag is
|
|
194
|
+
* forwarded.
|
|
195
|
+
*/
|
|
196
|
+
getBreakState(): {
|
|
197
|
+
shouldBreak: boolean;
|
|
198
|
+
reason?: string;
|
|
199
|
+
};
|
|
134
200
|
}
|
|
135
201
|
/**
|
|
136
202
|
* IExecutionRuntime — Interface for the runtime environment.
|
|
@@ -30,6 +30,7 @@ export interface ReactiveTarget {
|
|
|
30
30
|
addErrorInfo(key: string, value: unknown): void;
|
|
31
31
|
addMetric(name: string, value: unknown): void;
|
|
32
32
|
addEval(name: string, value: unknown): void;
|
|
33
|
+
emitEvent(name: string, payload?: unknown): void;
|
|
33
34
|
}
|
|
34
35
|
export interface ScopeMethods {
|
|
35
36
|
$getValue(key: string): unknown;
|
|
@@ -83,13 +84,70 @@ export interface ScopeMethods {
|
|
|
83
84
|
* when element types are known: `(arr as string[]).push(x)`.
|
|
84
85
|
*/
|
|
85
86
|
$batchArray(key: string, fn: (arr: unknown[]) => void): void;
|
|
86
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Fire a structured event to every attached recorder implementing
|
|
89
|
+
* `onEmit`. The primary primitive for consumer-authored observability
|
|
90
|
+
* events (LLM tokens, billing metrics, domain milestones, etc.) that
|
|
91
|
+
* shouldn't live in scope state.
|
|
92
|
+
*
|
|
93
|
+
* Synchronous, pass-through — delivered to recorders immediately, in
|
|
94
|
+
* call order. Zero-allocation when no recorders are attached.
|
|
95
|
+
*
|
|
96
|
+
* ## Naming convention
|
|
97
|
+
*
|
|
98
|
+
* Use hierarchical dotted names: `'<namespace>.<category>.<event>'`.
|
|
99
|
+
* Examples:
|
|
100
|
+
* - `'agentfootprint.llm.tokens'`
|
|
101
|
+
* - `'myapp.billing.spend'`
|
|
102
|
+
* - `'auth.check.denied'`
|
|
103
|
+
*
|
|
104
|
+
* The library stays vocabulary-agnostic — no central registry, no
|
|
105
|
+
* reserved prefixes. Namespace prefixes prevent collisions across
|
|
106
|
+
* libraries and apps naturally.
|
|
107
|
+
*
|
|
108
|
+
* ## Redaction
|
|
109
|
+
*
|
|
110
|
+
* If `RedactionPolicy.emitPatterns` matches the name, the payload is
|
|
111
|
+
* replaced with `'[REDACTED]'` before dispatch. No recorder ever sees
|
|
112
|
+
* the raw value for redacted event names.
|
|
113
|
+
*
|
|
114
|
+
* @param name - Consumer-chosen event identifier (see naming convention).
|
|
115
|
+
* @param payload - Structured data to attach. Shape is up to the
|
|
116
|
+
* consumer; library treats it as opaque.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* scope.$emit('agentfootprint.llm.tokens', { input: 100, output: 50 });
|
|
121
|
+
* scope.$emit('myapp.decision', { branch: 'approved', confidence: 0.92 });
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
$emit(name: string, payload?: unknown): void;
|
|
125
|
+
/**
|
|
126
|
+
* Stop the current execution context.
|
|
127
|
+
*
|
|
128
|
+
* - **In a top-level chart:** the traverser exits cleanly after this stage
|
|
129
|
+
* completes.
|
|
130
|
+
* - **Inside a subflow:** by default, only the subflow's own execution
|
|
131
|
+
* stops; control returns to the parent as normal. If the subflow is
|
|
132
|
+
* mounted with `propagateBreak: true`, the break signal (and its
|
|
133
|
+
* optional reason) propagates up to the parent scope, terminating the
|
|
134
|
+
* outer loop too. See `SubflowMountOptions.propagateBreak`.
|
|
135
|
+
*
|
|
136
|
+
* @param reason - Optional free-form string describing why the break
|
|
137
|
+
* happened. Propagates to `FlowBreakEvent.reason` so recorders and
|
|
138
|
+
* custom narratives can surface it. Defaults to undefined.
|
|
139
|
+
*/
|
|
140
|
+
$break(reason?: string): void;
|
|
87
141
|
$toRaw(): ReactiveTarget;
|
|
88
142
|
}
|
|
89
143
|
export type TypedScope<T extends object = Record<string, unknown>> = T & ScopeMethods;
|
|
90
144
|
export interface ReactiveOptions {
|
|
91
|
-
/**
|
|
92
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Pipeline break function — injected by StageRunner after scope creation.
|
|
147
|
+
* Accepts an optional reason string that propagates to `FlowBreakEvent`
|
|
148
|
+
* and (when `propagateBreak` is set on a mount) up to the parent scope.
|
|
149
|
+
*/
|
|
150
|
+
breakPipeline?: (reason?: string) => void;
|
|
93
151
|
}
|
|
94
152
|
export declare const SCOPE_METHOD_NAMES: Set<string>;
|
|
95
153
|
export declare const BREAK_SETTER: unique symbol;
|