@ghchinoy/litflow 0.2.7 → 0.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/CHANGELOG.md +59 -0
- package/dist/breadboard/data/common.d.ts +35 -0
- package/dist/breadboard/engine/loader/capability.d.ts +21 -0
- package/dist/breadboard/engine/loader/loader.d.ts +29 -0
- package/dist/breadboard/engine/loader/resolve-graph-urls.d.ts +16 -0
- package/dist/breadboard/engine/runtime/bubble.d.ts +23 -0
- package/dist/breadboard/engine/runtime/graph-based-node-handler.d.ts +16 -0
- package/dist/breadboard/engine/runtime/handler.d.ts +27 -0
- package/dist/breadboard/engine/runtime/harness/events.d.ts +145 -0
- package/dist/breadboard/engine/runtime/harness/local.d.ts +10 -0
- package/dist/breadboard/engine/runtime/harness/plan-runner.d.ts +25 -0
- package/dist/breadboard/engine/runtime/run/invoke-graph.d.ts +12 -0
- package/dist/breadboard/engine/runtime/run/node-invoker.d.ts +12 -0
- package/dist/breadboard/engine/runtime/run/run-graph.d.ts +12 -0
- package/dist/breadboard/engine/runtime/run.d.ts +29 -0
- package/dist/breadboard/engine/runtime/sandbox/capabilities-manager.d.ts +15 -0
- package/dist/breadboard/engine/runtime/sandbox/file-system-handler-factory.d.ts +15 -0
- package/dist/breadboard/engine/runtime/sandbox/invoke-describer.d.ts +10 -0
- package/dist/breadboard/engine/runtime/static/create-plan.d.ts +14 -0
- package/dist/breadboard/engine/runtime/static/orchestrator.d.ts +72 -0
- package/dist/breadboard/engine/runtime/traversal/index.d.ts +20 -0
- package/dist/breadboard/engine/runtime/traversal/iterator.d.ts +12 -0
- package/dist/breadboard/engine/runtime/traversal/machine.d.ts +14 -0
- package/dist/breadboard/engine/runtime/traversal/representation.d.ts +27 -0
- package/dist/breadboard/engine/runtime/traversal/result.d.ts +24 -0
- package/dist/breadboard/engine/runtime/traversal/state.d.ts +34 -0
- package/dist/breadboard/lit-flow-runner.d.ts +13 -0
- package/dist/breadboard/lit-flow-runner.test.d.ts +1 -0
- package/dist/breadboard/runner.d.ts +13 -0
- package/dist/index.d.ts +2 -0
- package/dist/lit-chiclet.d.ts +9 -0
- package/dist/lit-schema-node.d.ts +13 -0
- package/dist/lit-schema-node.test.d.ts +1 -0
- package/dist/litflow.js +708 -442
- package/dist/litflow.js.map +1 -1
- package/package.json +18 -4
- package/src/breadboard/data/common.ts +450 -0
- package/src/breadboard/data/file-system.ts +54 -0
- package/src/breadboard/data/inline-all-content.ts +126 -0
- package/src/breadboard/data/recent-boards.ts +118 -0
- package/src/breadboard/data/save-outputs-as-file.ts +104 -0
- package/src/breadboard/engine/add-run-module.ts +168 -0
- package/src/breadboard/engine/editor/blank.ts +65 -0
- package/src/breadboard/engine/editor/edge.ts +27 -0
- package/src/breadboard/engine/editor/events.ts +64 -0
- package/src/breadboard/engine/editor/graph.ts +383 -0
- package/src/breadboard/engine/editor/history.ts +98 -0
- package/src/breadboard/engine/editor/index.ts +8 -0
- package/src/breadboard/engine/editor/operations/add-asset.ts +45 -0
- package/src/breadboard/engine/editor/operations/add-edge.ts +142 -0
- package/src/breadboard/engine/editor/operations/add-graph.ts +47 -0
- package/src/breadboard/engine/editor/operations/add-module.ts +64 -0
- package/src/breadboard/engine/editor/operations/add-node.ts +86 -0
- package/src/breadboard/engine/editor/operations/change-asset-metadata.ts +70 -0
- package/src/breadboard/engine/editor/operations/change-configuration.ts +82 -0
- package/src/breadboard/engine/editor/operations/change-edge-metadata.ts +58 -0
- package/src/breadboard/engine/editor/operations/change-edge.ts +111 -0
- package/src/breadboard/engine/editor/operations/change-graph-metadata.ts +52 -0
- package/src/breadboard/engine/editor/operations/change-metadata.ts +92 -0
- package/src/breadboard/engine/editor/operations/change-module.ts +64 -0
- package/src/breadboard/engine/editor/operations/error.ts +21 -0
- package/src/breadboard/engine/editor/operations/remove-asset.ts +48 -0
- package/src/breadboard/engine/editor/operations/remove-edge.ts +89 -0
- package/src/breadboard/engine/editor/operations/remove-graph.ts +49 -0
- package/src/breadboard/engine/editor/operations/remove-integration.ts +54 -0
- package/src/breadboard/engine/editor/operations/remove-module.ts +69 -0
- package/src/breadboard/engine/editor/operations/remove-node.ts +86 -0
- package/src/breadboard/engine/editor/operations/replace-graph.ts +52 -0
- package/src/breadboard/engine/editor/operations/toggle-export.ts +72 -0
- package/src/breadboard/engine/editor/operations/upsert-integration.ts +43 -0
- package/src/breadboard/engine/editor/selection.ts +58 -0
- package/src/breadboard/engine/editor/transforms/configure-sidewire.ts +73 -0
- package/src/breadboard/engine/editor/transforms/isolate-selection.ts +54 -0
- package/src/breadboard/engine/editor/transforms/merge-graph.ts +58 -0
- package/src/breadboard/engine/editor/transforms/move-to-graph.ts +102 -0
- package/src/breadboard/engine/editor/transforms/move-to-new-graph.ts +72 -0
- package/src/breadboard/engine/editor/transforms/sidewire-to-new-graph.ts +82 -0
- package/src/breadboard/engine/file-system/blob-transform.ts +44 -0
- package/src/breadboard/engine/file-system/composed-peristent-backend.ts +140 -0
- package/src/breadboard/engine/file-system/ephemeral-blob-store.ts +46 -0
- package/src/breadboard/engine/file-system/in-memory-blob-store.ts +87 -0
- package/src/breadboard/engine/file-system/index.ts +723 -0
- package/src/breadboard/engine/file-system/partial-persistent-backend.ts +109 -0
- package/src/breadboard/engine/file-system/path.ts +125 -0
- package/src/breadboard/engine/file-system/persistent-file.ts +66 -0
- package/src/breadboard/engine/file-system/readable-stream-file.ts +61 -0
- package/src/breadboard/engine/file-system/stub-file-system.ts +47 -0
- package/src/breadboard/engine/file-system/utils.ts +40 -0
- package/src/breadboard/engine/inspector/graph/bubbled-node.ts +162 -0
- package/src/breadboard/engine/inspector/graph/describe-cache.ts +78 -0
- package/src/breadboard/engine/inspector/graph/describe-type-cache.ts +48 -0
- package/src/breadboard/engine/inspector/graph/edge-cache.ts +118 -0
- package/src/breadboard/engine/inspector/graph/edge.ts +133 -0
- package/src/breadboard/engine/inspector/graph/event.ts +35 -0
- package/src/breadboard/engine/inspector/graph/exports-describer.ts +45 -0
- package/src/breadboard/engine/inspector/graph/graph-cache.ts +54 -0
- package/src/breadboard/engine/inspector/graph/graph-describer-manager.ts +338 -0
- package/src/breadboard/engine/inspector/graph/graph-descriptor-handle.ts +73 -0
- package/src/breadboard/engine/inspector/graph/graph-node-type.ts +111 -0
- package/src/breadboard/engine/inspector/graph/graph-queries.ts +256 -0
- package/src/breadboard/engine/inspector/graph/graph.ts +163 -0
- package/src/breadboard/engine/inspector/graph/inspectable-asset.ts +36 -0
- package/src/breadboard/engine/inspector/graph/kits.ts +208 -0
- package/src/breadboard/engine/inspector/graph/module.ts +69 -0
- package/src/breadboard/engine/inspector/graph/mutable-graph.ts +150 -0
- package/src/breadboard/engine/inspector/graph/node-cache.ts +123 -0
- package/src/breadboard/engine/inspector/graph/node-describer-manager.ts +279 -0
- package/src/breadboard/engine/inspector/graph/node-type-describer-manager.ts +122 -0
- package/src/breadboard/engine/inspector/graph/node.ts +149 -0
- package/src/breadboard/engine/inspector/graph/port-cache.ts +80 -0
- package/src/breadboard/engine/inspector/graph/ports.ts +292 -0
- package/src/breadboard/engine/inspector/graph/schemas.ts +131 -0
- package/src/breadboard/engine/inspector/graph/virtual-node.ts +184 -0
- package/src/breadboard/engine/inspector/graph-store.ts +629 -0
- package/src/breadboard/engine/inspector/index.ts +13 -0
- package/src/breadboard/engine/inspector/utils.ts +20 -0
- package/src/breadboard/engine/loader/capability.ts +184 -0
- package/src/breadboard/engine/loader/index.ts +14 -0
- package/src/breadboard/engine/loader/loader.ts +244 -0
- package/src/breadboard/engine/loader/resolve-graph-urls.ts +111 -0
- package/src/breadboard/engine/runtime/bubble.ts +269 -0
- package/src/breadboard/engine/runtime/graph-based-node-handler.ts +174 -0
- package/src/breadboard/engine/runtime/handler.ts +166 -0
- package/src/breadboard/engine/runtime/harness/diagnostics.ts +22 -0
- package/src/breadboard/engine/runtime/harness/events.ts +217 -0
- package/src/breadboard/engine/runtime/harness/index.ts +14 -0
- package/src/breadboard/engine/runtime/harness/local.ts +48 -0
- package/src/breadboard/engine/runtime/harness/plan-runner.ts +759 -0
- package/src/breadboard/engine/runtime/index.ts +8 -0
- package/src/breadboard/engine/runtime/legacy.ts +28 -0
- package/src/breadboard/engine/runtime/run/invoke-graph.ts +79 -0
- package/src/breadboard/engine/runtime/run/node-invoker.ts +137 -0
- package/src/breadboard/engine/runtime/run/run-graph.ts +186 -0
- package/src/breadboard/engine/runtime/run.ts +111 -0
- package/src/breadboard/engine/runtime/sandbox/capabilities-manager.ts +253 -0
- package/src/breadboard/engine/runtime/sandbox/file-system-handler-factory.ts +53 -0
- package/src/breadboard/engine/runtime/sandbox/invoke-describer.ts +125 -0
- package/src/breadboard/engine/runtime/static/condense.ts +155 -0
- package/src/breadboard/engine/runtime/static/create-plan.ts +134 -0
- package/src/breadboard/engine/runtime/static/nodes-to-subgraph.ts +168 -0
- package/src/breadboard/engine/runtime/static/orchestrator.ts +664 -0
- package/src/breadboard/engine/runtime/static/types.ts +77 -0
- package/src/breadboard/engine/runtime/traversal/index.ts +58 -0
- package/src/breadboard/engine/runtime/traversal/iterator.ts +124 -0
- package/src/breadboard/engine/runtime/traversal/machine.ts +58 -0
- package/src/breadboard/engine/runtime/traversal/representation.ts +115 -0
- package/src/breadboard/engine/runtime/traversal/result.ts +72 -0
- package/src/breadboard/engine/runtime/traversal/state.ts +115 -0
- package/src/breadboard/engine/telemetry.ts +121 -0
- package/src/breadboard/engine/types.ts +32 -0
- package/src/breadboard/lit-flow-runner.test.ts +44 -0
- package/src/breadboard/lit-flow-runner.ts +98 -0
- package/src/breadboard/runner.ts +80 -0
- package/src/index.ts +2 -0
- package/src/lit-chiclet.ts +69 -0
- package/src/lit-flow.ts +17 -7
- package/src/lit-schema-node.test.ts +65 -0
- package/src/lit-schema-node.ts +194 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2025 Google LLC
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
InputValues,
|
|
10
|
+
NodeIdentifier,
|
|
11
|
+
Outcome,
|
|
12
|
+
OutputValues,
|
|
13
|
+
OrchestrationPlan,
|
|
14
|
+
NodeLifecycleState,
|
|
15
|
+
OrchestratorProgress,
|
|
16
|
+
PlanNodeInfo,
|
|
17
|
+
Task,
|
|
18
|
+
OrchestrationNodeInfo,
|
|
19
|
+
OrchestratorState,
|
|
20
|
+
OrchestratorCallbacks,
|
|
21
|
+
OrchestratorNodeState,
|
|
22
|
+
} from "@breadboard-ai/types";
|
|
23
|
+
import { err, ok } from "@breadboard-ai/utils";
|
|
24
|
+
import { Signal } from "signal-polyfill";
|
|
25
|
+
|
|
26
|
+
export { Orchestrator };
|
|
27
|
+
|
|
28
|
+
type NodeInternalState = {
|
|
29
|
+
readonly plan: PlanNodeInfo;
|
|
30
|
+
readonly stage: number;
|
|
31
|
+
state: NodeLifecycleState;
|
|
32
|
+
inputs: InputValues | null;
|
|
33
|
+
outputs: OutputValues | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type InternalOrchestratorState = Map<NodeIdentifier, NodeInternalState>;
|
|
37
|
+
|
|
38
|
+
const STATES_WITH_OUTPUTS: ReadonlySet<NodeLifecycleState> = new Set([
|
|
39
|
+
"succeeded",
|
|
40
|
+
"failed",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const TERMINAL_STATES: ReadonlySet<NodeLifecycleState> = new Set([
|
|
44
|
+
"succeeded",
|
|
45
|
+
"failed",
|
|
46
|
+
"skipped",
|
|
47
|
+
"interrupted",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* States where a node is currently doing something.
|
|
52
|
+
*/
|
|
53
|
+
const IN_PROGRESS_STATES: ReadonlySet<NodeLifecycleState> = new Set([
|
|
54
|
+
"working",
|
|
55
|
+
"waiting",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const PROCESSING_STATES: ReadonlySet<NodeLifecycleState> = new Set([
|
|
59
|
+
"ready",
|
|
60
|
+
...IN_PROGRESS_STATES,
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* States from which a node can become "working" again.
|
|
65
|
+
*/
|
|
66
|
+
const WORKABLE_STATES: ReadonlySet<NodeLifecycleState> = new Set([
|
|
67
|
+
"succeeded",
|
|
68
|
+
"failed",
|
|
69
|
+
"interrupted",
|
|
70
|
+
...PROCESSING_STATES,
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* States from which a node can be restarted.
|
|
75
|
+
*/
|
|
76
|
+
const RESTARTABLE_STATES: ReadonlySet<NodeLifecycleState> = new Set([
|
|
77
|
+
"ready",
|
|
78
|
+
"succeeded",
|
|
79
|
+
"failed",
|
|
80
|
+
"interrupted",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
type StageAdvancementResult = "done" | "more";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The Orchestrator acts as the state machine for running a graph.
|
|
87
|
+
* Its primary responsibilities are:
|
|
88
|
+
*
|
|
89
|
+
* 1. Lifecycle Management: Starting, resetting, and managing the overall
|
|
90
|
+
* progress of a run from beginning to end.
|
|
91
|
+
* 2. Task Coordination: Determining which nodes are ready to be invoked
|
|
92
|
+
* based on dependencies.
|
|
93
|
+
* 3. State Persistence: Receiving results of node invocation and persisting
|
|
94
|
+
* the state workflow.
|
|
95
|
+
* 4. Inspection and Debugging: Providing methods to observe the current state
|
|
96
|
+
* of a run, inspect cached results, and control execution flow.
|
|
97
|
+
*
|
|
98
|
+
* It breaks down the process into three distinct parts:
|
|
99
|
+
* - the planning -- determining the static sequence of a run
|
|
100
|
+
* - the orchestration -- managing execution results that can be dynamic
|
|
101
|
+
* - actual node invocation
|
|
102
|
+
*/
|
|
103
|
+
class Orchestrator {
|
|
104
|
+
readonly #state: InternalOrchestratorState = new Map();
|
|
105
|
+
#currentStage: number = 0;
|
|
106
|
+
#progress: OrchestratorProgress = "initial";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A signal to manage changes to the orchestrator state, so that this
|
|
110
|
+
* class can be used with signals.
|
|
111
|
+
*/
|
|
112
|
+
readonly #changed = new Signal.State({});
|
|
113
|
+
|
|
114
|
+
constructor(
|
|
115
|
+
public readonly plan: OrchestrationPlan,
|
|
116
|
+
public readonly callbacks: OrchestratorCallbacks
|
|
117
|
+
) {
|
|
118
|
+
this.reset();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns current progress of the orchestration.
|
|
123
|
+
*/
|
|
124
|
+
get progress() {
|
|
125
|
+
this.#changed.get();
|
|
126
|
+
return this.#progress;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get working() {
|
|
130
|
+
this.#changed.get();
|
|
131
|
+
return Array.from(this.#state.values()).some((nodeState) => {
|
|
132
|
+
return IN_PROGRESS_STATES.has(nodeState.state);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get allWorking() {
|
|
137
|
+
return Array.from(this.fullState().entries()).filter(([, nodeState]) => {
|
|
138
|
+
return nodeState.state === "working";
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get allWaiting() {
|
|
143
|
+
return Array.from(this.fullState().entries()).filter(([, nodeState]) => {
|
|
144
|
+
return nodeState.state === "waiting";
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get failed() {
|
|
149
|
+
this.#changed.get();
|
|
150
|
+
return Array.from(this.#state.values()).some((nodeState) => {
|
|
151
|
+
return nodeState.state === "failed" || nodeState.state === "interrupted";
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getNodeState(id: NodeIdentifier): OrchestratorNodeState | null {
|
|
156
|
+
const nodeState = this.#state.get(id);
|
|
157
|
+
if (!nodeState) return null;
|
|
158
|
+
|
|
159
|
+
return this.#createOrchestratorNodeState(nodeState);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Bring the orchestrator to the initial state.
|
|
164
|
+
*/
|
|
165
|
+
reset(): Outcome<void> {
|
|
166
|
+
this.#changed.set({});
|
|
167
|
+
this.#state.clear();
|
|
168
|
+
this.#resetAtStage(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#resetAtStage(starting: number) {
|
|
172
|
+
this.#changed.set({});
|
|
173
|
+
const orchestratorState = this.#state;
|
|
174
|
+
const stagesToReset = this.plan.stages.slice(starting);
|
|
175
|
+
try {
|
|
176
|
+
stagesToReset.forEach((stage, index) => {
|
|
177
|
+
const firstStage = index === 0;
|
|
178
|
+
stage.forEach((plan: PlanNodeInfo) => {
|
|
179
|
+
const nodeState = orchestratorState.get(plan.node.id);
|
|
180
|
+
const inputs = firstStage ? nodeState?.inputs || {} : null;
|
|
181
|
+
const existingState = nodeState?.state || "inactive";
|
|
182
|
+
let state: NodeLifecycleState;
|
|
183
|
+
if (IN_PROGRESS_STATES.has(existingState)) {
|
|
184
|
+
state = existingState;
|
|
185
|
+
} else {
|
|
186
|
+
state = firstStage ? "ready" : "inactive";
|
|
187
|
+
}
|
|
188
|
+
orchestratorState.set(plan.node.id, {
|
|
189
|
+
stage: starting + index,
|
|
190
|
+
state,
|
|
191
|
+
plan,
|
|
192
|
+
inputs,
|
|
193
|
+
outputs: null,
|
|
194
|
+
});
|
|
195
|
+
this.callbacks.stateChangedbyOrchestrator?.(plan.node.id, state);
|
|
196
|
+
this.callbacks.stateChanged?.(state, plan);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
} catch (e) {
|
|
200
|
+
return err((e as Error).message);
|
|
201
|
+
}
|
|
202
|
+
this.#currentStage = starting;
|
|
203
|
+
this.#progress = starting == 0 ? "initial" : "advanced";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
restartAtCurrentStage(): Outcome<void> {
|
|
207
|
+
return this.restartAtStage(this.#currentStage);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
restartAtStage(stage: number): Outcome<void> {
|
|
211
|
+
if (stage < 0) {
|
|
212
|
+
return this.reset();
|
|
213
|
+
}
|
|
214
|
+
if (stage > this.#currentStage) {
|
|
215
|
+
return err(`Stage ${stage} is beyond the current stage`);
|
|
216
|
+
}
|
|
217
|
+
return this.#resetAtStage(stage);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
restartAtNode(id: NodeIdentifier): Outcome<void> {
|
|
221
|
+
this.#changed.set({});
|
|
222
|
+
const state = this.#state.get(id);
|
|
223
|
+
if (!state) {
|
|
224
|
+
return err(`Unable to restart at node "${id}": node not found`);
|
|
225
|
+
}
|
|
226
|
+
if (!RESTARTABLE_STATES.has(state.state)) {
|
|
227
|
+
return err(
|
|
228
|
+
`Unable to restart at a node "${id}": node state is "${state.state}"`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const stage = state.stage;
|
|
232
|
+
|
|
233
|
+
// 1. Save outputs at the stage.
|
|
234
|
+
const outputs: Map<NodeIdentifier, OutputValues> = new Map();
|
|
235
|
+
try {
|
|
236
|
+
this.plan.stages[stage].forEach((plan) => {
|
|
237
|
+
const nodeId = plan.node.id;
|
|
238
|
+
if (nodeId === id) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const state = this.#state.get(nodeId);
|
|
242
|
+
if (!state) {
|
|
243
|
+
throw new Error(`Unable to restart at node "${id}": node not found`);
|
|
244
|
+
}
|
|
245
|
+
if (!state.outputs) return;
|
|
246
|
+
outputs.set(nodeId, state.outputs);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const restarting = this.restartAtStage(stage);
|
|
250
|
+
if (!ok(restarting)) return restarting;
|
|
251
|
+
|
|
252
|
+
outputs.forEach((outputs, nodeId) => {
|
|
253
|
+
const providing = this.provideOutputs(nodeId, outputs);
|
|
254
|
+
if (!ok(providing)) {
|
|
255
|
+
throw new Error(providing.$error);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
} catch (e) {
|
|
259
|
+
return err((e as Error).message);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Now, let's mark the node we're restarting from as "ready". This is
|
|
263
|
+
// important, because providing outputs will change the node states
|
|
264
|
+
// and mark our starting node as "skipped".
|
|
265
|
+
const newState = this.#state.get(id);
|
|
266
|
+
if (!newState) {
|
|
267
|
+
return err(`Unable to restart at node "${id}": node not found`);
|
|
268
|
+
}
|
|
269
|
+
this.#updateNodeState(newState, "ready", false);
|
|
270
|
+
|
|
271
|
+
// Finally, let's bring the stage back to the one where the starting node
|
|
272
|
+
// is. Providing outputs will advance the stages and we may actually end up
|
|
273
|
+
// in some future state. Let's roll that back and get back to ours.
|
|
274
|
+
this.#currentStage = stage;
|
|
275
|
+
this.#progress = stage == 0 ? "initial" : "advanced";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
setWorking(id: NodeIdentifier): Outcome<void> {
|
|
279
|
+
const state = this.#state.get(id);
|
|
280
|
+
if (state?.state === "working") return;
|
|
281
|
+
|
|
282
|
+
if (!state) {
|
|
283
|
+
return err(`Unable to set node "${id}" to working: node not found`);
|
|
284
|
+
}
|
|
285
|
+
if (!WORKABLE_STATES.has(state.state)) {
|
|
286
|
+
return err(
|
|
287
|
+
`Unable to set node "${id}" to working: not in workable state`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
this.#changed.set({});
|
|
291
|
+
this.#updateNodeState(state, "working", true);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setWaiting(id: NodeIdentifier): Outcome<void> {
|
|
295
|
+
const state = this.#state.get(id);
|
|
296
|
+
if (state?.state === "waiting") return;
|
|
297
|
+
|
|
298
|
+
if (!state) {
|
|
299
|
+
return err(`Unable to set node "${id}" to waiting: node not found`);
|
|
300
|
+
}
|
|
301
|
+
if (!WORKABLE_STATES.has(state.state)) {
|
|
302
|
+
return err(
|
|
303
|
+
`Unable to set node "${id}" to waiting: not in workable state`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
this.#changed.set({});
|
|
307
|
+
this.#updateNodeState(state, "waiting", true);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setInterrupted(id: NodeIdentifier): Outcome<void> {
|
|
311
|
+
const state = this.#state.get(id);
|
|
312
|
+
if (state?.state === "interrupted") return;
|
|
313
|
+
|
|
314
|
+
if (!state) {
|
|
315
|
+
return err(`Unable to set node "${id}" to interrupted: node not found`);
|
|
316
|
+
}
|
|
317
|
+
if (!IN_PROGRESS_STATES.has(state.state)) {
|
|
318
|
+
return err(
|
|
319
|
+
`Unable to set node "${id}" to interrupted: not working or waiting`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
this.#changed.set({});
|
|
323
|
+
this.#updateNodeState(state, "interrupted", true);
|
|
324
|
+
this.#propagateSkip(state);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#createOrchestratorNodeState(
|
|
328
|
+
internal: NodeInternalState
|
|
329
|
+
): OrchestratorNodeState {
|
|
330
|
+
return {
|
|
331
|
+
node: internal.plan.node,
|
|
332
|
+
state: internal.state,
|
|
333
|
+
stage: internal.stage,
|
|
334
|
+
inputs: internal.inputs,
|
|
335
|
+
outputs: internal.outputs,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fullState(): OrchestratorState {
|
|
340
|
+
this.#changed.get();
|
|
341
|
+
return new Map(
|
|
342
|
+
Array.from(this.#state.entries()).map(([id, internal]) => [
|
|
343
|
+
id,
|
|
344
|
+
this.#createOrchestratorNodeState(internal),
|
|
345
|
+
])
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Provides a way to inspect the current state of nodes as they are being
|
|
351
|
+
* orchestrated.
|
|
352
|
+
* @returns a map representing current state of all nodes
|
|
353
|
+
*/
|
|
354
|
+
state(): ReadonlyMap<NodeIdentifier, OrchestrationNodeInfo> {
|
|
355
|
+
this.#changed.get();
|
|
356
|
+
return new Map(
|
|
357
|
+
Array.from(this.#state.entries()).map(([id, internal]) => {
|
|
358
|
+
return [id, { node: internal.plan.node, state: internal.state }];
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Creates a new task to invoke a node, given a node id
|
|
365
|
+
* @param id -- node id
|
|
366
|
+
*/
|
|
367
|
+
taskFromId(id: NodeIdentifier): Outcome<Task> {
|
|
368
|
+
this.#changed.get();
|
|
369
|
+
const state = this.#state.get(id);
|
|
370
|
+
if (!state) {
|
|
371
|
+
return err(`Unknown node id "${id}"`);
|
|
372
|
+
}
|
|
373
|
+
if (!state.inputs) {
|
|
374
|
+
return err(`Node has no inputs`);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
node: state.plan.node,
|
|
378
|
+
inputs: state.inputs,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Creates a list of current tasks: nodes to be invoked next, along
|
|
384
|
+
* with their inputs, according to the current state of the orchestrator.
|
|
385
|
+
*/
|
|
386
|
+
currentTasks(): Outcome<Task[]> {
|
|
387
|
+
this.#changed.get();
|
|
388
|
+
const tasks: Task[] = [];
|
|
389
|
+
const stage = this.plan.stages[this.#currentStage];
|
|
390
|
+
if (!stage) {
|
|
391
|
+
return tasks;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
stage.forEach((plan) => {
|
|
395
|
+
const state = this.#state.get(plan.node.id);
|
|
396
|
+
if (!state) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`While getting current tasks, node "${plan.node.id}" was not found`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (state.state === "ready") {
|
|
402
|
+
tasks.push({ node: plan.node, inputs: state.inputs! });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return tasks;
|
|
406
|
+
} catch (e) {
|
|
407
|
+
return err((e as Error).message);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#propagateSkip(state: NodeInternalState): Outcome<void> {
|
|
412
|
+
this.#changed.set({});
|
|
413
|
+
try {
|
|
414
|
+
// First, propagate the "skipped" state downstream to all descendants.
|
|
415
|
+
const queue: NodeInternalState[] = [state];
|
|
416
|
+
const visited: Set<NodeIdentifier> = new Set();
|
|
417
|
+
while (queue.length > 0) {
|
|
418
|
+
const current = queue.shift()!;
|
|
419
|
+
current.plan.downstream.forEach((dep) => {
|
|
420
|
+
const id = dep.to;
|
|
421
|
+
if (visited.has(id)) return;
|
|
422
|
+
const target = this.#state.get(id);
|
|
423
|
+
if (!target) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`While trying to propagate skip downstream, failed to retrieve target state`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (!TERMINAL_STATES.has(target.state)) {
|
|
429
|
+
queue.push(target);
|
|
430
|
+
this.#updateNodeState(target, "skipped", false);
|
|
431
|
+
}
|
|
432
|
+
visited.add(id);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
// Then, propagate the "skipped" state through the graph, until we
|
|
436
|
+
// reach quiescence.
|
|
437
|
+
let changed = true;
|
|
438
|
+
while (changed) {
|
|
439
|
+
changed = false;
|
|
440
|
+
this.#state.forEach((state) => {
|
|
441
|
+
if (TERMINAL_STATES.has(state.state)) return;
|
|
442
|
+
if (state.state === "working" || state.state === "waiting") return;
|
|
443
|
+
if (state.plan.downstream.length === 0) return;
|
|
444
|
+
|
|
445
|
+
const allTerminal = state.plan.downstream.every((dep) => {
|
|
446
|
+
const target = this.#state.get(dep.to);
|
|
447
|
+
if (!target) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`While trying to settle state, failed to retrieve target state`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return TERMINAL_STATES.has(target.state);
|
|
453
|
+
});
|
|
454
|
+
if (allTerminal) {
|
|
455
|
+
this.#updateNodeState(state, "skipped", false);
|
|
456
|
+
changed = true;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {
|
|
461
|
+
return err((e as Error).message);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#tryAdvancingStage(): Outcome<StageAdvancementResult> {
|
|
466
|
+
this.#changed.set({});
|
|
467
|
+
// Check to see if all other nodes at this stage have been invoked
|
|
468
|
+
// (the state will be set to something other than "ready")
|
|
469
|
+
const currentStage = this.plan.stages[this.#currentStage];
|
|
470
|
+
if (!currentStage) {
|
|
471
|
+
return err(
|
|
472
|
+
`While trying to advance stage, failed to retrieve current stage`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let hasWaiting;
|
|
477
|
+
|
|
478
|
+
const complete = currentStage.every((plan) => {
|
|
479
|
+
const state = this.#state.get(plan.node.id)?.state;
|
|
480
|
+
if (!state) return false;
|
|
481
|
+
|
|
482
|
+
if (IN_PROGRESS_STATES.has(state)) {
|
|
483
|
+
hasWaiting = true;
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
return state !== "ready";
|
|
487
|
+
});
|
|
488
|
+
// Nope, still work to do.
|
|
489
|
+
if (!complete) {
|
|
490
|
+
this.#progress = "working";
|
|
491
|
+
return hasWaiting ? "done" : "more";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const nextStageIndex = this.#currentStage + 1;
|
|
496
|
+
if (nextStageIndex > this.plan.stages.length - 1) {
|
|
497
|
+
this.#progress = "finished";
|
|
498
|
+
return "done";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const stage = this.plan.stages[nextStageIndex];
|
|
502
|
+
stage.forEach((info) => {
|
|
503
|
+
const state = this.#state.get(info.node.id);
|
|
504
|
+
if (!state) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`While trying to advance stage, failed to retrieve current state`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const inputs: InputValues = {};
|
|
510
|
+
let upstreamSkipped = false;
|
|
511
|
+
info.upstream.forEach((dep) => {
|
|
512
|
+
if (upstreamSkipped) return;
|
|
513
|
+
|
|
514
|
+
const from = this.#state.get(dep.from);
|
|
515
|
+
if (!from) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`While trying to advance stage, failed to retrieve upstream state`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (from.state === "inactive") {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`While trying to advance stage, found node ${from.plan.node.id} with unresolved dependencies`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
if (from.state === "ready") {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`While trying to advance stage, found node ${from.plan.node.id} what was not yet invoked`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
if (from.state === "skipped" || from.state === "failed") {
|
|
531
|
+
upstreamSkipped = true;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const input = from.outputs?.[dep.out || ""];
|
|
535
|
+
if (input && dep.in) {
|
|
536
|
+
inputs[dep.in] = input;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
if (
|
|
540
|
+
upstreamSkipped ||
|
|
541
|
+
Object.keys(inputs).length !== info.upstream.length
|
|
542
|
+
) {
|
|
543
|
+
// Either we were already skipped upstream or Some inputs were
|
|
544
|
+
// missing, so we're going to mark this node as Skipped.
|
|
545
|
+
this.#updateNodeState(state, "skipped", false);
|
|
546
|
+
const propagating = this.#propagateSkip(state);
|
|
547
|
+
if (!ok(propagating)) return propagating;
|
|
548
|
+
} else {
|
|
549
|
+
this.#updateNodeState(state, "ready", false);
|
|
550
|
+
state.inputs = inputs;
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
this.#currentStage = nextStageIndex;
|
|
554
|
+
|
|
555
|
+
// Propagate outputs from the current stage as inputs for the next stage.
|
|
556
|
+
this.#progress = "advanced";
|
|
557
|
+
return "more";
|
|
558
|
+
} catch (e) {
|
|
559
|
+
return err((e as Error).message);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
#updateNodeState(
|
|
564
|
+
node: NodeInternalState,
|
|
565
|
+
state: NodeLifecycleState,
|
|
566
|
+
changedByConsumer: boolean
|
|
567
|
+
) {
|
|
568
|
+
node.state = state;
|
|
569
|
+
this.callbacks.stateChanged?.(state, node.plan);
|
|
570
|
+
if (!changedByConsumer) {
|
|
571
|
+
const id = node.plan.node.id;
|
|
572
|
+
this.callbacks.stateChangedbyOrchestrator?.(
|
|
573
|
+
id,
|
|
574
|
+
state,
|
|
575
|
+
node.outputs?.["$error"]
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
update(old: Orchestrator) {
|
|
581
|
+
const oldEntries = [...(old.#state.entries() || [])];
|
|
582
|
+
if (oldEntries.length === 0) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
let index = 0;
|
|
586
|
+
for (const [id, newNodeState] of this.#state) {
|
|
587
|
+
const [oldId, oldNodeState] = oldEntries[index] || [];
|
|
588
|
+
if (id === oldId) {
|
|
589
|
+
if (
|
|
590
|
+
oldNodeState.outputs &&
|
|
591
|
+
STATES_WITH_OUTPUTS.has(oldNodeState.state)
|
|
592
|
+
) {
|
|
593
|
+
this.provideOutputs(id, oldNodeState.outputs);
|
|
594
|
+
} else {
|
|
595
|
+
newNodeState.state = oldNodeState.state;
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
if (index === 0) {
|
|
599
|
+
// Account for the situation when the topology changes so that
|
|
600
|
+
// there are no similarities in plan. In this case, we make the
|
|
601
|
+
// first node as "interrupted", so that it doesn't trigger
|
|
602
|
+
// continuation of a run if we're in the middle of one.
|
|
603
|
+
newNodeState.state = "interrupted";
|
|
604
|
+
}
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
index++;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Submit results of a node invocation. Also updates the current state.
|
|
613
|
+
*/
|
|
614
|
+
provideOutputs(
|
|
615
|
+
id: NodeIdentifier,
|
|
616
|
+
outputs: OutputValues
|
|
617
|
+
): Outcome<OrchestratorProgress> {
|
|
618
|
+
this.#changed.set({});
|
|
619
|
+
const state = this.#state.get(id);
|
|
620
|
+
if (!state) {
|
|
621
|
+
return err(
|
|
622
|
+
`While providing outputs, couldn't get state for node "${id}"`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
let earlierStage = false;
|
|
626
|
+
if (state.stage < this.#currentStage) {
|
|
627
|
+
earlierStage = true;
|
|
628
|
+
} else if (state.stage > this.#currentStage) {
|
|
629
|
+
return err(`Can't provide outputs to later stages`);
|
|
630
|
+
}
|
|
631
|
+
if (state.state === "waiting") {
|
|
632
|
+
return err(`Can't provide outputs while the node is waiting for input`);
|
|
633
|
+
}
|
|
634
|
+
// Update state of the node.
|
|
635
|
+
state.outputs = outputs;
|
|
636
|
+
if ("$error" in outputs) {
|
|
637
|
+
this.#updateNodeState(state, "failed", false);
|
|
638
|
+
if (earlierStage) return this.#progress;
|
|
639
|
+
|
|
640
|
+
const propagating = this.#propagateSkip(state);
|
|
641
|
+
if (!ok(propagating)) return propagating;
|
|
642
|
+
} else {
|
|
643
|
+
this.#updateNodeState(state, "succeeded", false);
|
|
644
|
+
if (earlierStage) {
|
|
645
|
+
// Jump back to the node's stage, so that we propagate
|
|
646
|
+
// from it.
|
|
647
|
+
this.#currentStage = state.stage;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
let progress;
|
|
652
|
+
for (;;) {
|
|
653
|
+
progress = this.#tryAdvancingStage();
|
|
654
|
+
if (!ok(progress)) return progress;
|
|
655
|
+
if (progress === "done") break;
|
|
656
|
+
|
|
657
|
+
const hasWork = this.currentTasks();
|
|
658
|
+
if (!ok(hasWork)) return hasWork;
|
|
659
|
+
|
|
660
|
+
if (hasWork.length > 0) break;
|
|
661
|
+
}
|
|
662
|
+
return this.#progress;
|
|
663
|
+
}
|
|
664
|
+
}
|