footprintjs 4.1.0 → 4.3.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 +49 -1
- package/dist/esm/index.js +6 -1
- package/dist/esm/lib/builder/FlowChartBuilder.js +53 -1
- package/dist/esm/lib/builder/types.js +1 -1
- package/dist/esm/lib/engine/graph/StageNode.js +1 -1
- package/dist/esm/lib/engine/handlers/ChildrenExecutor.js +14 -1
- package/dist/esm/lib/engine/handlers/DeciderHandler.js +7 -1
- package/dist/esm/lib/engine/handlers/SelectorHandler.js +7 -1
- package/dist/esm/lib/engine/handlers/StageRunner.js +13 -1
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +8 -1
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +39 -1
- package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +31 -1
- package/dist/esm/lib/engine/narrative/NarrativeFlowRecorder.js +10 -1
- package/dist/esm/lib/engine/narrative/NullControlFlowNarrativeGenerator.js +3 -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 +8 -1
- package/dist/esm/lib/engine/types.js +2 -1
- package/dist/esm/lib/pause/index.js +2 -0
- package/dist/esm/lib/pause/types.js +58 -0
- package/dist/esm/lib/reactive/types.js +2 -1
- package/dist/esm/lib/recorder/CompositeRecorder.js +172 -0
- package/dist/esm/lib/recorder/index.js +2 -0
- package/dist/esm/lib/runner/ExecutionRuntime.js +11 -2
- package/dist/esm/lib/runner/FlowChartExecutor.js +260 -8
- package/dist/esm/lib/scope/ScopeFacade.js +23 -1
- package/dist/esm/lib/scope/recorders/DebugRecorder.js +36 -2
- package/dist/esm/lib/scope/recorders/MetricRecorder.js +60 -4
- package/dist/esm/lib/scope/types.js +1 -1
- package/dist/esm/recorders.js +4 -3
- package/dist/index.js +21 -15
- package/dist/lib/builder/FlowChartBuilder.js +53 -1
- package/dist/lib/builder/types.js +1 -1
- package/dist/lib/engine/graph/StageNode.js +1 -1
- package/dist/lib/engine/handlers/ChildrenExecutor.js +14 -1
- package/dist/lib/engine/handlers/DeciderHandler.js +7 -1
- package/dist/lib/engine/handlers/SelectorHandler.js +7 -1
- package/dist/lib/engine/handlers/StageRunner.js +17 -5
- package/dist/lib/engine/handlers/SubflowExecutor.js +8 -1
- package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +39 -1
- package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +31 -1
- package/dist/lib/engine/narrative/NarrativeFlowRecorder.js +10 -1
- package/dist/lib/engine/narrative/NullControlFlowNarrativeGenerator.js +3 -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 +8 -1
- package/dist/lib/engine/types.js +6 -2
- package/dist/lib/pause/index.js +8 -0
- package/dist/lib/pause/types.js +64 -0
- package/dist/lib/reactive/types.js +2 -1
- package/dist/lib/recorder/CompositeRecorder.js +176 -0
- package/dist/lib/recorder/index.js +6 -0
- package/dist/lib/runner/ExecutionRuntime.js +11 -2
- package/dist/lib/runner/FlowChartExecutor.js +260 -8
- package/dist/lib/scope/ScopeFacade.js +23 -1
- package/dist/lib/scope/recorders/DebugRecorder.js +36 -2
- package/dist/lib/scope/recorders/MetricRecorder.js +60 -4
- package/dist/lib/scope/types.js +1 -1
- package/dist/recorders.js +6 -4
- package/dist/types/index.d.ts +8 -1
- package/dist/types/lib/builder/FlowChartBuilder.d.ts +22 -0
- package/dist/types/lib/builder/types.d.ts +3 -1
- package/dist/types/lib/engine/graph/StageNode.d.ts +9 -0
- package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +9 -1
- package/dist/types/lib/engine/narrative/FlowRecorderDispatcher.d.ts +2 -0
- package/dist/types/lib/engine/narrative/NarrativeFlowRecorder.d.ts +3 -1
- package/dist/types/lib/engine/narrative/NullControlFlowNarrativeGenerator.d.ts +2 -0
- package/dist/types/lib/engine/narrative/narrativeTypes.d.ts +1 -1
- package/dist/types/lib/engine/narrative/types.d.ts +24 -0
- package/dist/types/lib/engine/types.d.ts +23 -0
- package/dist/types/lib/pause/index.d.ts +2 -0
- package/dist/types/lib/pause/types.d.ts +175 -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/ExecutionRuntime.d.ts +4 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +78 -2
- package/dist/types/lib/scope/ScopeFacade.d.ts +4 -0
- package/dist/types/lib/scope/recorders/DebugRecorder.d.ts +20 -2
- package/dist/types/lib/scope/recorders/MetricRecorder.d.ts +51 -3
- package/dist/types/lib/scope/types.d.ts +10 -0
- package/dist/types/recorders.d.ts +5 -2
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* it belongs in the runner layer (Phase 5).
|
|
13
13
|
*/
|
|
14
14
|
import type { ScopeFactory } from '../engine/types.js';
|
|
15
|
+
import type { PausableHandler } from '../pause/types.js';
|
|
15
16
|
import type { TypedScope } from '../reactive/types.js';
|
|
16
17
|
import { type RunnableFlowChart } from '../runner/RunnableChart.js';
|
|
17
18
|
import { type TypedStageFunction } from './typedFlowChart.js';
|
|
@@ -107,6 +108,27 @@ export declare class FlowChartBuilder<TOut = any, TScope = any> {
|
|
|
107
108
|
start(name: string, fn: StageFunction<TOut, TScope>, id: string, description?: string): this;
|
|
108
109
|
addFunction(name: string, fn: StageFunction<TOut, TScope>, id: string, description?: string): this;
|
|
109
110
|
addStreamingFunction(name: string, fn: StageFunction<TOut, TScope>, id: string, streamId?: string, description?: string): this;
|
|
111
|
+
/**
|
|
112
|
+
* Add a pausable stage — can pause execution and resume later with input.
|
|
113
|
+
*
|
|
114
|
+
* The handler has two phases:
|
|
115
|
+
* - `execute`: runs first time. Return `{ pause: true }` to pause.
|
|
116
|
+
* - `resume`: runs when the flowchart is resumed with input.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* .addPausableFunction('ApproveOrder', {
|
|
121
|
+
* execute: async (scope) => {
|
|
122
|
+
* scope.orderId = '123';
|
|
123
|
+
* return { pause: true, data: { question: 'Approve?' } };
|
|
124
|
+
* },
|
|
125
|
+
* resume: async (scope, input) => {
|
|
126
|
+
* scope.approved = input.approved;
|
|
127
|
+
* },
|
|
128
|
+
* }, 'approve-order', 'Manager approval gate')
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
addPausableFunction(name: string, handler: PausableHandler<TScope>, id: string, description?: string): this;
|
|
110
132
|
addDeciderFunction(name: string, fn: StageFunction<any, TScope>, id: string, description?: string): DeciderList<TOut, TScope>;
|
|
111
133
|
addSelectorFunction(name: string, fn: StageFunction<any, TScope>, id: string, description?: string): SelectorFnList<TOut, TScope>;
|
|
112
134
|
addListOfFunction(children: SimplifiedParallelSpec<TOut, TScope>[], options?: {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { StageNode } from '../engine/graph/StageNode.js';
|
|
12
12
|
import type { ILogger, ScopeFactory, StageFunction, TraversalExtractor } from '../engine/types.js';
|
|
13
13
|
import type { ScopeProtectionMode } from '../scope/protection/types.js';
|
|
14
|
-
export type { StageNode } from '../engine/graph/StageNode.js';
|
|
14
|
+
export type { ResumeFn, StageNode } from '../engine/graph/StageNode.js';
|
|
15
15
|
export type { ILogger, StageFunction, StreamCallback, StreamHandlers, StreamLifecycleHandler, StreamTokenHandler, SubflowMountOptions, } from '../engine/types.js';
|
|
16
16
|
/** Relaxed-generic alias for builder ergonomics. */
|
|
17
17
|
export type StageFn = StageFunction<any, any>;
|
|
@@ -49,6 +49,8 @@ export interface SerializedPipelineStructure {
|
|
|
49
49
|
isLazy?: boolean;
|
|
50
50
|
/** True when this node is a back-edge reference created by loopTo() — not an executable stage. */
|
|
51
51
|
isLoopReference?: boolean;
|
|
52
|
+
/** When true, this stage can pause execution (PausableHandler pattern). */
|
|
53
|
+
isPausable?: boolean;
|
|
52
54
|
}
|
|
53
55
|
export interface FlowChartSpec {
|
|
54
56
|
name: string;
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* - Isolated sub-traversals via `isSubflowRoot` (subflow)
|
|
10
10
|
*/
|
|
11
11
|
import type { StageFunction, SubflowMountOptions } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Resume function for pausable stages.
|
|
14
|
+
* Unlike StageFunction, the 2nd argument is the resume input (not breakFn).
|
|
15
|
+
*/
|
|
16
|
+
export type ResumeFn<TScope = any, TInput = unknown> = (scope: TScope, input: TInput) => Promise<void> | void;
|
|
12
17
|
/** Picks exactly ONE child by ID. Conditional branch (if/switch). */
|
|
13
18
|
export type Decider = (nodeArgs: any) => string | Promise<string>;
|
|
14
19
|
/**
|
|
@@ -59,6 +64,10 @@ export type StageNode<TOut = any, TScope = any> = {
|
|
|
59
64
|
subflowMountOptions?: SubflowMountOptions;
|
|
60
65
|
/** When true, parallel children use fail-fast semantics (reject on first error) */
|
|
61
66
|
failFast?: boolean;
|
|
67
|
+
/** When true, this stage can pause execution (PausableHandler pattern). */
|
|
68
|
+
isPausable?: boolean;
|
|
69
|
+
/** Resume function — called instead of fn when resuming a paused stage. */
|
|
70
|
+
resumeFn?: ResumeFn<TScope>;
|
|
62
71
|
/**
|
|
63
72
|
* True if this node is a back-edge reference created by loopTo() — not an executable stage.
|
|
64
73
|
* Serialization equivalent: `SerializedPipelineStructure.isLoopReference` (different name
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { ReadEvent, Recorder, WriteEvent } from '../../scope/types.js';
|
|
16
16
|
import type { CombinedNarrativeEntry, NarrativeRenderer } from './narrativeTypes.js';
|
|
17
|
-
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowRecorder, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent } from './types.js';
|
|
17
|
+
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowPauseEvent, FlowRecorder, FlowResumeEvent, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent } from './types.js';
|
|
18
18
|
export interface CombinedNarrativeRecorderOptions {
|
|
19
19
|
includeStepNumbers?: boolean;
|
|
20
20
|
includeValues?: boolean;
|
|
@@ -61,6 +61,14 @@ export declare class CombinedNarrativeRecorder implements FlowRecorder, Recorder
|
|
|
61
61
|
onSubflowExit(event: FlowSubflowEvent): void;
|
|
62
62
|
onLoop(event: FlowLoopEvent): void;
|
|
63
63
|
onBreak(event: FlowBreakEvent): void;
|
|
64
|
+
onPause(event: FlowPauseEvent | {
|
|
65
|
+
stageName?: string;
|
|
66
|
+
stageId?: string;
|
|
67
|
+
}): void;
|
|
68
|
+
onResume(event: FlowResumeEvent | {
|
|
69
|
+
stageName?: string;
|
|
70
|
+
stageId?: string;
|
|
71
|
+
}): void;
|
|
64
72
|
/**
|
|
65
73
|
* Handles errors from both channels:
|
|
66
74
|
* - FlowRecorder.onError (FlowErrorEvent with message + structuredError)
|
|
@@ -32,6 +32,8 @@ export declare class FlowRecorderDispatcher implements IControlFlowNarrative {
|
|
|
32
32
|
onLoop(targetStage: string, iteration: number, description?: string, traversalContext?: TraversalContext): void;
|
|
33
33
|
onBreak(stageName: string, traversalContext?: TraversalContext): void;
|
|
34
34
|
onError(stageName: string, errorMessage: string, error: unknown, traversalContext?: TraversalContext): void;
|
|
35
|
+
onPause(stageName: string, stageId: string, pauseData: unknown, subflowPath: readonly string[], traversalContext?: TraversalContext): void;
|
|
36
|
+
onResume(stageName: string, stageId: string, hasInput: boolean, traversalContext?: TraversalContext): void;
|
|
35
37
|
/**
|
|
36
38
|
* Returns sentences from an attached NarrativeFlowRecorder (looked up by ID).
|
|
37
39
|
* Callers that need sentences should attach a NarrativeFlowRecorder with id 'narrative'
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Consumers who want different narrative behavior (windowed loops, adaptive
|
|
9
9
|
* summarization, etc.) can replace this with a different FlowRecorder.
|
|
10
10
|
*/
|
|
11
|
-
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowNextEvent, FlowRecorder, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent } from './types.js';
|
|
11
|
+
import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowNextEvent, FlowPauseEvent, FlowRecorder, FlowResumeEvent, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent } from './types.js';
|
|
12
12
|
export declare class NarrativeFlowRecorder implements FlowRecorder {
|
|
13
13
|
readonly id: string;
|
|
14
14
|
private sentences;
|
|
@@ -25,6 +25,8 @@ export declare class NarrativeFlowRecorder implements FlowRecorder {
|
|
|
25
25
|
onLoop(event: FlowLoopEvent): void;
|
|
26
26
|
onBreak(event: FlowBreakEvent): void;
|
|
27
27
|
onError(event: FlowErrorEvent): void;
|
|
28
|
+
onPause(event: FlowPauseEvent): void;
|
|
29
|
+
onResume(event: FlowResumeEvent): void;
|
|
28
30
|
/** Returns a defensive copy of accumulated sentences. */
|
|
29
31
|
getSentences(): string[];
|
|
30
32
|
/** Clears accumulated sentences. Useful for reuse across runs. */
|
|
@@ -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';
|
|
9
|
+
type: 'stage' | 'step' | 'condition' | 'fork' | 'selector' | 'subflow' | 'loop' | 'break' | 'error' | 'pause' | 'resume';
|
|
10
10
|
text: string;
|
|
11
11
|
depth: number;
|
|
12
12
|
stageName?: string;
|
|
@@ -36,6 +36,10 @@ export interface IControlFlowNarrative {
|
|
|
36
36
|
onBreak(stageName: string, traversalContext?: TraversalContext): void;
|
|
37
37
|
/** Called when a stage throws an error. Raw error is extracted into structured details. */
|
|
38
38
|
onError(stageName: string, errorMessage: string, error: unknown, traversalContext?: TraversalContext): void;
|
|
39
|
+
/** Called when a pausable stage pauses execution. */
|
|
40
|
+
onPause(stageName: string, stageId: string, pauseData: unknown, subflowPath: readonly string[], traversalContext?: TraversalContext): void;
|
|
41
|
+
/** Called when a paused stage is resumed. */
|
|
42
|
+
onResume(stageName: string, stageId: string, hasInput: boolean, traversalContext?: TraversalContext): void;
|
|
39
43
|
/** Returns accumulated narrative sentences in execution order. */
|
|
40
44
|
getSentences(): string[];
|
|
41
45
|
}
|
|
@@ -145,6 +149,24 @@ export interface FlowErrorEvent {
|
|
|
145
149
|
structuredError: StructuredErrorInfo;
|
|
146
150
|
traversalContext?: TraversalContext;
|
|
147
151
|
}
|
|
152
|
+
/** Event passed to FlowRecorder.onPause. */
|
|
153
|
+
export interface FlowPauseEvent {
|
|
154
|
+
stageName: string;
|
|
155
|
+
stageId: string;
|
|
156
|
+
/** Data from the pause signal (question, reason, metadata). */
|
|
157
|
+
pauseData?: unknown;
|
|
158
|
+
/** Path through subflows to the paused stage. Empty at root level. */
|
|
159
|
+
subflowPath: readonly string[];
|
|
160
|
+
traversalContext?: TraversalContext;
|
|
161
|
+
}
|
|
162
|
+
/** Event passed to FlowRecorder.onResume. */
|
|
163
|
+
export interface FlowResumeEvent {
|
|
164
|
+
stageName: string;
|
|
165
|
+
stageId: string;
|
|
166
|
+
/** Whether resume input was provided. */
|
|
167
|
+
hasInput: boolean;
|
|
168
|
+
traversalContext?: TraversalContext;
|
|
169
|
+
}
|
|
148
170
|
/**
|
|
149
171
|
* FlowRecorder — Pluggable observer for control flow events.
|
|
150
172
|
*
|
|
@@ -177,6 +199,8 @@ export interface FlowRecorder {
|
|
|
177
199
|
onLoop?(event: FlowLoopEvent): void;
|
|
178
200
|
onBreak?(event: FlowBreakEvent): void;
|
|
179
201
|
onError?(event: FlowErrorEvent): void;
|
|
202
|
+
onPause?(event: FlowPauseEvent): void;
|
|
203
|
+
onResume?(event: FlowResumeEvent): void;
|
|
180
204
|
/** Called before each run to reset per-run state. Implement for stateful recorders. */
|
|
181
205
|
clear?(): void;
|
|
182
206
|
/** Optional: expose collected data for inclusion in snapshots. */
|
|
@@ -11,6 +11,8 @@ import type { ScopeProtectionMode } from '../scope/protection/types.js';
|
|
|
11
11
|
import type { StageNode } from './graph/StageNode.js';
|
|
12
12
|
import type { IControlFlowNarrative } from './narrative/types.js';
|
|
13
13
|
export type { Decider, Selector, StageNode } from './graph/StageNode.js';
|
|
14
|
+
export type { FlowchartCheckpoint, PausableHandler, PauseResult } from '../pause/index.js';
|
|
15
|
+
export { isPauseResult, isPauseSignal, PauseSignal } from '../pause/index.js';
|
|
14
16
|
/** Minimal logging contract. Mirrors Console API subset. */
|
|
15
17
|
export interface ILogger {
|
|
16
18
|
info(message?: any, ...optionalParams: any[]): void;
|
|
@@ -48,6 +50,18 @@ export interface SubflowMountOptions<TParentScope = any, TSubflowInput = any, TS
|
|
|
48
50
|
* `[...existing, ...value]` for arrays. Return only the **delta** (new items)
|
|
49
51
|
* for array keys, or the parent's existing array items will be duplicated.
|
|
50
52
|
* Scalar values are replaced normally.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // WRONG — returns full array, parent concats → duplicates
|
|
57
|
+
* outputMapper: (sf) => ({ messages: sf.allMessages })
|
|
58
|
+
*
|
|
59
|
+
* // RIGHT — returns only new items (delta), concat appends correctly
|
|
60
|
+
* outputMapper: (sf) => ({ messages: sf.newMessages })
|
|
61
|
+
*
|
|
62
|
+
* // Scalars are fine — replaced, not concatenated
|
|
63
|
+
* outputMapper: (sf) => ({ status: sf.result, count: sf.total })
|
|
64
|
+
* ```
|
|
51
65
|
*/
|
|
52
66
|
outputMapper?: (subflowOutput: TSubflowOutput, parentScope: TParentScope) => Record<string, unknown>;
|
|
53
67
|
}
|
|
@@ -243,6 +257,13 @@ export type BranchResults = {
|
|
|
243
257
|
[branchId: string]: BranchResult;
|
|
244
258
|
};
|
|
245
259
|
export type TraversalResult = BranchResults | string | Error;
|
|
260
|
+
/** Returned by run()/resume() when execution pauses. */
|
|
261
|
+
export type PausedResult = {
|
|
262
|
+
readonly paused: true;
|
|
263
|
+
readonly checkpoint: import('../pause/types.js').FlowchartCheckpoint;
|
|
264
|
+
};
|
|
265
|
+
/** Full return type of FlowChartExecutor.run() and resume(). */
|
|
266
|
+
export type ExecutorResult = TraversalResult | PausedResult;
|
|
246
267
|
export interface SerializedPipelineNode {
|
|
247
268
|
name: string;
|
|
248
269
|
id: string;
|
|
@@ -264,6 +285,8 @@ export interface SerializedPipelineNode {
|
|
|
264
285
|
isParallelChild?: boolean;
|
|
265
286
|
parallelGroupId?: string;
|
|
266
287
|
isDynamic?: boolean;
|
|
288
|
+
/** When true, this stage can pause execution (PausableHandler pattern). */
|
|
289
|
+
isPausable?: boolean;
|
|
267
290
|
}
|
|
268
291
|
export type FlowChart<TOut = any, TScope = any> = {
|
|
269
292
|
root: StageNode<TOut, TScope>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pause/Resume — serializable checkpoint for long-running or human-in-the-loop flows.
|
|
3
|
+
*
|
|
4
|
+
* A stage signals pause by calling `scope.$pause(data)` which throws a PauseSignal.
|
|
5
|
+
* The signal bubbles up through SubflowExecutor → FlowchartTraverser → FlowChartExecutor,
|
|
6
|
+
* each level adding its subflow ID to the path.
|
|
7
|
+
*
|
|
8
|
+
* The checkpoint captures:
|
|
9
|
+
* - pausedPath: full path to the paused stage (e.g., ['sf-payment', 'approve'])
|
|
10
|
+
* - sharedState: scope at the pause point
|
|
11
|
+
* - executionTree: completed stages for BTS/narrative
|
|
12
|
+
* - pauseData: question, reason, or metadata from $pause()
|
|
13
|
+
*
|
|
14
|
+
* Resume rebuilds the flowchart, restores scope, navigates to the paused stage,
|
|
15
|
+
* injects resumeInput, and continues traversal.
|
|
16
|
+
*
|
|
17
|
+
* Supported topologies: linear, subflow, lazy subflow, loop, nested subflow in loop.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Thrown by `scope.$pause()` to signal that execution should stop
|
|
21
|
+
* and create a serializable checkpoint.
|
|
22
|
+
*
|
|
23
|
+
* Bubbles up through SubflowExecutor (which prepends subflow ID to path)
|
|
24
|
+
* and is caught by FlowchartTraverser/FlowChartExecutor.
|
|
25
|
+
*/
|
|
26
|
+
export declare class PauseSignal extends Error {
|
|
27
|
+
/** Data from $pause() — question, reason, metadata. */
|
|
28
|
+
readonly pauseData: unknown;
|
|
29
|
+
/** ID of the stage that called $pause(). */
|
|
30
|
+
readonly stageId: string;
|
|
31
|
+
/** Path through subflows to the paused stage. Built during bubble-up. */
|
|
32
|
+
private _subflowPath;
|
|
33
|
+
constructor(data: unknown, stageId: string);
|
|
34
|
+
get subflowPath(): readonly string[];
|
|
35
|
+
/** Prepend a subflow ID to the path (called during bubble-up). */
|
|
36
|
+
prependSubflow(subflowId: string): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returned by a pausable stage's execute/resume function to signal pause.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* execute: async (scope) => {
|
|
44
|
+
* scope.orderId = '123';
|
|
45
|
+
* return { pause: true, data: { question: 'Approve order 123?' } };
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export interface PauseResult {
|
|
50
|
+
readonly pause: true;
|
|
51
|
+
/** Data to include in the checkpoint — question, reason, metadata. */
|
|
52
|
+
readonly data?: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Serializable checkpoint — everything needed to resume a paused flowchart.
|
|
56
|
+
*
|
|
57
|
+
* JSON-safe: no functions, no class instances, no SDK clients.
|
|
58
|
+
* Store anywhere: Redis, Postgres, localStorage, a file.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Save
|
|
63
|
+
* const checkpoint = executor.getCheckpoint(); // after pause
|
|
64
|
+
* await redis.set(`session:${id}`, JSON.stringify(checkpoint));
|
|
65
|
+
*
|
|
66
|
+
* // Resume (hours later, possibly different server)
|
|
67
|
+
* const checkpoint = JSON.parse(await redis.get(`session:${id}`));
|
|
68
|
+
* const executor = new FlowChartExecutor(chart);
|
|
69
|
+
* await executor.resume(checkpoint, { approved: true });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Serializable checkpoint — everything needed to resume a paused flowchart.
|
|
74
|
+
*
|
|
75
|
+
* The execution tree IS the traversed path. The leaf node with status 'paused'
|
|
76
|
+
* IS the cursor. No separate path array needed — the tree structure captures
|
|
77
|
+
* the full nesting (including subflows).
|
|
78
|
+
*
|
|
79
|
+
* JSON-safe: no functions, no class instances, no SDK clients.
|
|
80
|
+
* Store anywhere: Redis, Postgres, localStorage, a file.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const checkpoint = executor.getCheckpoint(); // after pause
|
|
85
|
+
* await redis.set(`session:${id}`, JSON.stringify(checkpoint));
|
|
86
|
+
*
|
|
87
|
+
* // Resume (hours later, possibly different server)
|
|
88
|
+
* const checkpoint = JSON.parse(await redis.get(`session:${id}`));
|
|
89
|
+
* const executor = new FlowChartExecutor(chart);
|
|
90
|
+
* await executor.resume(checkpoint, { approved: true });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export interface FlowchartCheckpoint {
|
|
94
|
+
/** Scope state at the pause point — all shared memory key/values. */
|
|
95
|
+
readonly sharedState: Record<string, unknown>;
|
|
96
|
+
/** Execution tree — the traversed path. The leaf with status 'paused' is the cursor.
|
|
97
|
+
* Contains subflow nesting. Used for BTS visualization and to find the resume point. */
|
|
98
|
+
readonly executionTree: unknown;
|
|
99
|
+
/** ID of the stage that paused. Used by resume() to find the node in the graph. */
|
|
100
|
+
readonly pausedStageId: string;
|
|
101
|
+
/** Path through subflows to the paused stage (e.g., ['sf-payment', 'sf-validation']).
|
|
102
|
+
* Empty array when paused at the top level. */
|
|
103
|
+
readonly subflowPath: readonly string[];
|
|
104
|
+
/** Data from $pause() — question, reason, metadata. */
|
|
105
|
+
readonly pauseData?: unknown;
|
|
106
|
+
/** Subflow results collected before the pause. */
|
|
107
|
+
readonly subflowResults?: Record<string, unknown>;
|
|
108
|
+
/** Timestamp of when the pause occurred. */
|
|
109
|
+
readonly pausedAt: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Handler for a pausable stage — has two phases: execute and resume.
|
|
113
|
+
*
|
|
114
|
+
* `execute` runs the first time. It can return `{ pause: true }` to pause.
|
|
115
|
+
* `resume` runs when the flowchart is resumed. It receives the resume input.
|
|
116
|
+
*
|
|
117
|
+
* Both phases receive the same scope. After execute pauses, the scope state
|
|
118
|
+
* is preserved in the checkpoint. On resume, the scope is restored before
|
|
119
|
+
* calling resume.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* .addPausableFunction('ApproveOrder', {
|
|
124
|
+
* execute: async (scope) => {
|
|
125
|
+
* scope.orderId = '123';
|
|
126
|
+
* scope.amount = 299;
|
|
127
|
+
* return { pause: true, data: { question: `Approve $${scope.amount} refund?` } };
|
|
128
|
+
* },
|
|
129
|
+
* resume: async (scope, input) => {
|
|
130
|
+
* scope.approved = input.approved;
|
|
131
|
+
* scope.approver = input.approver;
|
|
132
|
+
* },
|
|
133
|
+
* }, 'approve-order', 'Manager approval gate')
|
|
134
|
+
*
|
|
135
|
+
* // Later — resume with human's answer
|
|
136
|
+
* await executor.resume(checkpoint, { approved: true, approver: 'Jane' });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export interface PausableHandler<TScope = any, TInput = unknown> {
|
|
140
|
+
/**
|
|
141
|
+
* First-run phase. Return data to pause, or void/undefined to continue normally.
|
|
142
|
+
*
|
|
143
|
+
* Any non-void return value becomes the `pauseData` in the checkpoint.
|
|
144
|
+
* The library detects the return and pauses automatically — no need to
|
|
145
|
+
* call `pause()` or construct `{ pause: true }`.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* execute: async (scope) => {
|
|
150
|
+
* scope.orderId = '123';
|
|
151
|
+
* return { question: `Approve order ${scope.orderId}?` }; // ← pauses
|
|
152
|
+
* }
|
|
153
|
+
*
|
|
154
|
+
* // Conditional pause
|
|
155
|
+
* execute: async (scope) => {
|
|
156
|
+
* if (scope.amount > 500) {
|
|
157
|
+
* return { reason: 'High-value order needs approval' }; // ← pauses
|
|
158
|
+
* }
|
|
159
|
+
* // void return → no pause, continues normally
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
execute: (scope: TScope) => Promise<unknown> | unknown;
|
|
164
|
+
/**
|
|
165
|
+
* Resume phase. Called with the resume input when execution continues.
|
|
166
|
+
*
|
|
167
|
+
* The scope is restored from the checkpoint's `sharedState`. Writes during
|
|
168
|
+
* `resume` are committed and visible to subsequent stages.
|
|
169
|
+
*/
|
|
170
|
+
resume: (scope: TScope, input: TInput) => Promise<void> | void;
|
|
171
|
+
}
|
|
172
|
+
/** Check if a value is a PauseResult (stage wants to pause). */
|
|
173
|
+
export declare function isPauseResult(value: unknown): value is PauseResult;
|
|
174
|
+
/** Check if an error is a PauseSignal. Uses instanceof + name brand fallback for cross-realm safety. */
|
|
175
|
+
export declare function isPauseSignal(error: unknown): error is PauseSignal;
|
|
@@ -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
|
+
}
|
|
@@ -32,7 +32,11 @@ export declare class ExecutionRuntime {
|
|
|
32
32
|
globalStore: SharedMemory;
|
|
33
33
|
rootStageContext: StageContext;
|
|
34
34
|
executionHistory: EventLog;
|
|
35
|
+
/** Original root for getSnapshot() — set before resume changes rootStageContext. */
|
|
36
|
+
private _snapshotRoot?;
|
|
35
37
|
constructor(rootName: string, rootId: string, defaultValues?: unknown, initialState?: unknown);
|
|
38
|
+
/** Preserve the current rootStageContext for snapshots before changing it for resume. */
|
|
39
|
+
preserveSnapshotRoot(): void;
|
|
36
40
|
getPipelines(): string[];
|
|
37
41
|
setRootObject(path: string[], key: string, value: unknown): void;
|
|
38
42
|
getSnapshot(): RuntimeSnapshot;
|
|
@@ -20,7 +20,8 @@ import type { CombinedNarrativeRecorderOptions } from '../engine/narrative/Combi
|
|
|
20
20
|
import type { CombinedNarrativeEntry } from '../engine/narrative/narrativeTypes.js';
|
|
21
21
|
import type { ManifestEntry } from '../engine/narrative/recorders/ManifestFlowRecorder.js';
|
|
22
22
|
import type { FlowRecorder } from '../engine/narrative/types.js';
|
|
23
|
-
import { type ExtractorError, type FlowChart, type RunOptions, type ScopeFactory, type SerializedPipelineStructure, type StageNode, type StreamHandlers, type SubflowResult
|
|
23
|
+
import { type ExecutorResult, type ExtractorError, type FlowChart, type RunOptions, type ScopeFactory, type SerializedPipelineStructure, type StageNode, type StreamHandlers, type SubflowResult } from '../engine/types.js';
|
|
24
|
+
import type { FlowchartCheckpoint } from '../pause/types.js';
|
|
24
25
|
import type { ScopeProtectionMode } from '../scope/protection/types.js';
|
|
25
26
|
import type { Recorder, RedactionPolicy, RedactionReport } from '../scope/types.js';
|
|
26
27
|
import { type RuntimeSnapshot } from './ExecutionRuntime.js';
|
|
@@ -88,6 +89,7 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
88
89
|
private redactionPolicy;
|
|
89
90
|
private sharedRedactedKeys;
|
|
90
91
|
private sharedRedactedFieldsByKey;
|
|
92
|
+
private lastCheckpoint;
|
|
91
93
|
private readonly flowChartArgs;
|
|
92
94
|
/**
|
|
93
95
|
* Create a FlowChartExecutor.
|
|
@@ -118,10 +120,82 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
118
120
|
* most recent run. Never includes actual values.
|
|
119
121
|
*/
|
|
120
122
|
getRedactionReport(): RedactionReport;
|
|
123
|
+
/**
|
|
124
|
+
* Returns the checkpoint from the most recent paused execution, or `undefined`
|
|
125
|
+
* if the last run completed without pausing.
|
|
126
|
+
*
|
|
127
|
+
* The checkpoint is JSON-serializable — store it in Redis, Postgres, localStorage, etc.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const result = await executor.run({ input });
|
|
132
|
+
* if (executor.isPaused()) {
|
|
133
|
+
* const checkpoint = executor.getCheckpoint()!;
|
|
134
|
+
* await redis.set(`session:${id}`, JSON.stringify(checkpoint));
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
getCheckpoint(): FlowchartCheckpoint | undefined;
|
|
139
|
+
/** Returns `true` if the most recent run() was paused (checkpoint available). */
|
|
140
|
+
isPaused(): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Resume a paused flowchart from a checkpoint.
|
|
143
|
+
*
|
|
144
|
+
* Restores the scope state, calls the paused stage's `resumeFn` with the
|
|
145
|
+
* provided input, then continues traversal from the next stage.
|
|
146
|
+
*
|
|
147
|
+
* The checkpoint can come from `getCheckpoint()` on a previous run, or from
|
|
148
|
+
* a serialized checkpoint stored in Redis/Postgres/localStorage.
|
|
149
|
+
*
|
|
150
|
+
* **Narrative/recorder state is reset on resume.** To keep a unified narrative
|
|
151
|
+
* across pause/resume cycles, collect it before calling resume.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // After a pause...
|
|
156
|
+
* const checkpoint = executor.getCheckpoint()!;
|
|
157
|
+
* await redis.set(`session:${id}`, JSON.stringify(checkpoint));
|
|
158
|
+
*
|
|
159
|
+
* // Later (possibly different server, same chart)
|
|
160
|
+
* const checkpoint = JSON.parse(await redis.get(`session:${id}`));
|
|
161
|
+
* const executor = new FlowChartExecutor(chart);
|
|
162
|
+
* const result = await executor.resume(checkpoint, { approved: true });
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
resume(checkpoint: FlowchartCheckpoint, resumeInput?: unknown, options?: Pick<RunOptions, 'signal' | 'env' | 'maxDepth'>): Promise<ExecutorResult>;
|
|
166
|
+
/**
|
|
167
|
+
* Find a StageNode in the compiled graph by ID.
|
|
168
|
+
* Handles subflow paths by drilling into registered subflows.
|
|
169
|
+
*/
|
|
170
|
+
private findNodeInGraph;
|
|
171
|
+
/** DFS search for a node by ID in the StageNode graph. Cycle-safe via visited set. */
|
|
172
|
+
private dfsFind;
|
|
121
173
|
/**
|
|
122
174
|
* Attach a scope Recorder to observe data operations (reads, writes, commits).
|
|
123
175
|
* Automatically attached to every ScopeFacade created during traversal.
|
|
124
176
|
* Must be called before run().
|
|
177
|
+
*
|
|
178
|
+
* **Idempotent by ID:** If a recorder with the same `id` is already attached,
|
|
179
|
+
* it is replaced (not duplicated). This prevents double-counting when both
|
|
180
|
+
* a framework and the user attach the same recorder type.
|
|
181
|
+
*
|
|
182
|
+
* Built-in recorders use auto-increment IDs (`metrics-1`, `debug-1`, ...) by
|
|
183
|
+
* default, so multiple instances with different configs coexist. To override
|
|
184
|
+
* a framework-attached recorder, pass the same well-known ID.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* // Multiple recorders with different configs — each gets a unique ID
|
|
189
|
+
* executor.attachRecorder(new MetricRecorder());
|
|
190
|
+
* executor.attachRecorder(new DebugRecorder({ verbosity: 'minimal' }));
|
|
191
|
+
*
|
|
192
|
+
* // Override a framework-attached recorder by passing its well-known ID
|
|
193
|
+
* executor.attachRecorder(new MetricRecorder('metrics'));
|
|
194
|
+
*
|
|
195
|
+
* // Attaching twice with same ID replaces (no double-counting)
|
|
196
|
+
* executor.attachRecorder(new MetricRecorder('my-metrics'));
|
|
197
|
+
* executor.attachRecorder(new MetricRecorder('my-metrics')); // replaces previous
|
|
198
|
+
* ```
|
|
125
199
|
*/
|
|
126
200
|
attachRecorder(recorder: Recorder): void;
|
|
127
201
|
/** Detach all scope Recorders with the given ID. */
|
|
@@ -132,6 +206,8 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
132
206
|
* Attach a FlowRecorder to observe control flow events.
|
|
133
207
|
* Automatically enables narrative if not already enabled.
|
|
134
208
|
* Must be called before run() — recorders are passed to the traverser at creation time.
|
|
209
|
+
*
|
|
210
|
+
* **Idempotent by ID:** replaces existing recorder with same `id`.
|
|
135
211
|
*/
|
|
136
212
|
attachFlowRecorder(recorder: FlowRecorder): void;
|
|
137
213
|
/** Detach all FlowRecorders with the given ID. */
|
|
@@ -169,7 +245,7 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
169
245
|
* chart with N stages you will typically get more entries here than from `getNarrative()`.
|
|
170
246
|
*/
|
|
171
247
|
getFlowNarrative(): string[];
|
|
172
|
-
run(options?: RunOptions): Promise<
|
|
248
|
+
run(options?: RunOptions): Promise<ExecutorResult>;
|
|
173
249
|
getSnapshot(): RuntimeSnapshot;
|
|
174
250
|
/** @internal */
|
|
175
251
|
getRuntime(): import("../engine/types.js").IExecutionRuntime;
|