@codemation/core 0.3.0 → 0.4.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 +15 -0
- package/dist/{EngineRuntimeRegistration.types-Bjeo7Sfq.d.ts → EngineRuntimeRegistration.types-DU6MsjU9.d.ts} +2 -2
- package/dist/{EngineWorkflowRunnerService-Dd4yD31l.d.cts → EngineWorkflowRunnerService-BBkL4VQF.d.cts} +2 -2
- package/dist/{InMemoryRunDataFactory-OUzDmAHt.d.cts → InMemoryRunDataFactory-CsYEMJK2.d.cts} +11 -3
- package/dist/{RunIntentService-Bkg4oYrM.d.cts → RunIntentService-BvlTpmEb.d.cts} +224 -237
- package/dist/{RunIntentService-BAKikN8h.d.ts → RunIntentService-zbTchO9T.d.ts} +305 -259
- package/dist/bootstrap/index.cjs +2 -2
- package/dist/bootstrap/index.d.cts +19 -7
- package/dist/bootstrap/index.d.ts +3 -3
- package/dist/bootstrap/index.js +2 -2
- package/dist/{bootstrap-DwS5S7s9.cjs → bootstrap-DHH2uo-W.cjs} +4 -2
- package/dist/bootstrap-DHH2uo-W.cjs.map +1 -0
- package/dist/{bootstrap-BD6CobHl.js → bootstrap-DbUlOl11.js} +4 -2
- package/dist/bootstrap-DbUlOl11.js.map +1 -0
- package/dist/{index-uCm9l0nw.d.ts → index-CUt13qs1.d.ts} +62 -34
- package/dist/index.cjs +22 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -42
- package/dist/index.d.ts +3 -3
- package/dist/index.js +13 -16
- package/dist/index.js.map +1 -1
- package/dist/{runtime-Cy-3FTI_.js → runtime-BdH94eBR.js} +502 -123
- package/dist/runtime-BdH94eBR.js.map +1 -0
- package/dist/{runtime-ZJUpWmPH.cjs → runtime-feFn8OmG.cjs} +561 -122
- package/dist/runtime-feFn8OmG.cjs.map +1 -0
- package/dist/testing.cjs +40 -36
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +17 -26
- package/dist/testing.d.ts +17 -26
- package/dist/testing.js +40 -36
- package/dist/testing.js.map +1 -1
- package/dist/{workflowActivationPolicy-BzyzXLa_.cjs → workflowActivationPolicy-6V3OJD3N.cjs} +65 -19
- package/dist/workflowActivationPolicy-6V3OJD3N.cjs.map +1 -0
- package/dist/{workflowActivationPolicy-B8HzTk3o.js → workflowActivationPolicy-Td9HTOuD.js} +65 -19
- package/dist/workflowActivationPolicy-Td9HTOuD.js.map +1 -0
- package/package.json +2 -1
- package/src/ai/AgentConfigInspectorFactory.ts +4 -0
- package/src/ai/AgentMessageConfigNormalizerFactory.ts +7 -0
- package/src/ai/AgentToolFactory.ts +2 -2
- package/src/ai/AiHost.ts +11 -10
- package/src/ai/NodeBackedToolConfig.ts +1 -1
- package/src/authoring/defineNode.types.ts +48 -72
- package/src/authoring/index.ts +1 -1
- package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +8 -0
- package/src/contracts/emitPorts.ts +27 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/itemMeta.ts +11 -0
- package/src/contracts/itemValue.ts +147 -0
- package/src/contracts/runtimeTypes.ts +39 -22
- package/src/contracts/workflowTypes.ts +26 -56
- package/src/execution/FanInMergeByOriginMerger.ts +67 -0
- package/src/execution/ItemValueResolver.ts +27 -0
- package/src/execution/NodeActivationRequestComposer.ts +25 -0
- package/src/execution/NodeActivationRequestInputPreparer.ts +57 -25
- package/src/execution/NodeExecutor.ts +199 -30
- package/src/execution/NodeOutputNormalizer.ts +90 -0
- package/src/execution/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/orchestration/NodeExecutionRequestHandlerService.ts +39 -18
- package/src/orchestration/RunContinuationService.ts +11 -17
- package/src/planning/CurrentStateFrontierPlanner.ts +20 -20
- package/src/planning/RunQueuePlanner.ts +56 -19
- package/src/planning/WorkflowTopologyPlanner.ts +57 -33
- package/src/testing/ItemHarnessNode.ts +4 -10
- package/src/testing/ItemHarnessNodeConfig.ts +7 -16
- package/src/testing/RegistrarEngineTestKitFactory.ts +2 -0
- package/src/testing/SubWorkflowRunnerTestNode.ts +28 -43
- package/src/testing/SwitchHarnessNode.ts +54 -0
- package/src/types/index.ts +3 -0
- package/src/workflow/dsl/ChainCursorResolver.ts +68 -23
- package/src/workflow/dsl/WorkflowBuilder.ts +3 -5
- package/src/workflow/dsl/workflowBuilderTypes.ts +5 -8
- package/src/workflowSnapshots/MissingRuntimeNode.ts +4 -4
- package/src/workflowSnapshots/MissingRuntimeNodeConfig.ts +2 -2
- package/src/workflowSnapshots/WorkflowSnapshotCodec.ts +16 -7
- package/dist/bootstrap-BD6CobHl.js.map +0 -1
- package/dist/bootstrap-DwS5S7s9.cjs.map +0 -1
- package/dist/runtime-Cy-3FTI_.js.map +0 -1
- package/dist/runtime-ZJUpWmPH.cjs.map +0 -1
- package/dist/workflowActivationPolicy-B8HzTk3o.js.map +0 -1
- package/dist/workflowActivationPolicy-BzyzXLa_.cjs.map +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isPortsEmission, isUnbrandedPortsEmissionShape } from "../contracts/emitPorts";
|
|
2
|
+
import type { PortsEmission } from "../contracts/emitPorts";
|
|
3
|
+
import type { Item, JsonNonArray, LineageCarryPolicy, NodeOutputs, OutputPortKey, Items } from "../types";
|
|
4
|
+
|
|
5
|
+
export class NodeOutputNormalizer {
|
|
6
|
+
normalizeExecuteResult(
|
|
7
|
+
args: Readonly<{
|
|
8
|
+
baseItem: Item;
|
|
9
|
+
raw: unknown;
|
|
10
|
+
carry: LineageCarryPolicy;
|
|
11
|
+
}>,
|
|
12
|
+
): NodeOutputs {
|
|
13
|
+
const { baseItem, raw, carry } = args;
|
|
14
|
+
if (isPortsEmission(raw)) {
|
|
15
|
+
return this.emitPortsToOutputs(baseItem, raw, carry);
|
|
16
|
+
}
|
|
17
|
+
if (isUnbrandedPortsEmissionShape(raw)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"execute() returned an unbranded `{ ports: ... }` object. Use emitPorts(...) for multi-port runnable outputs.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(raw)) {
|
|
23
|
+
return this.arrayFanOutToMain(baseItem, raw, carry);
|
|
24
|
+
}
|
|
25
|
+
if (this.isItemLike(raw)) {
|
|
26
|
+
return { main: [this.applyLineage(baseItem, raw, carry)] };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
main: [this.applyLineage(baseItem, { json: raw as JsonNonArray }, carry)],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private arrayFanOutToMain(baseItem: Item, raw: readonly unknown[], carry: LineageCarryPolicy): NodeOutputs {
|
|
34
|
+
for (const el of raw) {
|
|
35
|
+
if (Array.isArray(el)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"execute() fan-out arrays must contain only non-array JSON elements (nested arrays belong inside objects).",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const main: Item[] = raw.map((json) => this.applyLineage(baseItem, { json: json as JsonNonArray }, carry));
|
|
42
|
+
return { main };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private emitPortsToOutputs(baseItem: Item, emission: PortsEmission, carry: LineageCarryPolicy): NodeOutputs {
|
|
46
|
+
const out: NodeOutputs = {};
|
|
47
|
+
for (const [port, payload] of Object.entries(emission.ports)) {
|
|
48
|
+
if (payload === undefined) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
out[port as OutputPortKey] = this.normalizePortPayload(baseItem, payload, carry);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private normalizePortPayload(
|
|
57
|
+
baseItem: Item,
|
|
58
|
+
payload: Items | ReadonlyArray<JsonNonArray>,
|
|
59
|
+
carry: LineageCarryPolicy,
|
|
60
|
+
): Items {
|
|
61
|
+
if (payload.length === 0) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const el0 = payload[0] as unknown;
|
|
65
|
+
if (this.isItemLike(el0)) {
|
|
66
|
+
return (payload as Items).map((it) => this.applyLineage(baseItem, it, carry));
|
|
67
|
+
}
|
|
68
|
+
return (payload as readonly JsonNonArray[]).map((json) => this.applyLineage(baseItem, { json }, carry));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private isItemLike(value: unknown): value is Item {
|
|
72
|
+
return typeof value === "object" && value !== null && "json" in value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private applyLineage(baseItem: Item, next: Item, carry: LineageCarryPolicy): Item {
|
|
76
|
+
if (carry === "carryThrough") {
|
|
77
|
+
return {
|
|
78
|
+
...baseItem,
|
|
79
|
+
...next,
|
|
80
|
+
json: next.json,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
json: next.json,
|
|
85
|
+
...(next.binary ? { binary: next.binary } : {}),
|
|
86
|
+
...(next.meta ? { meta: next.meta } : {}),
|
|
87
|
+
...(next.paired ? { paired: next.paired } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/execution/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { CredentialResolverFactory } from "./CredentialResolverFactory";
|
|
|
5
5
|
export { DefaultAsyncSleeper } from "./DefaultAsyncSleeper";
|
|
6
6
|
export { DefaultExecutionContextFactory } from "./DefaultExecutionContextFactory";
|
|
7
7
|
export { InProcessRetryRunner } from "./InProcessRetryRunner";
|
|
8
|
+
export { ItemValueResolver } from "./ItemValueResolver";
|
|
9
|
+
export { NodeOutputNormalizer } from "./NodeOutputNormalizer";
|
|
8
10
|
export { InProcessRetryRunnerFactory } from "./InProcessRetryRunnerFactory";
|
|
9
11
|
export { NodeActivationRequestComposer } from "./NodeActivationRequestComposer";
|
|
10
12
|
export { NodeExecutionSnapshotFactory } from "./NodeExecutionSnapshotFactory";
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export {
|
|
|
18
18
|
DefaultAsyncSleeper,
|
|
19
19
|
DefaultExecutionContextFactory,
|
|
20
20
|
InProcessRetryRunner,
|
|
21
|
+
ItemValueResolver,
|
|
22
|
+
NodeOutputNormalizer,
|
|
21
23
|
} from "./execution";
|
|
22
24
|
export { EngineExecutionLimitsPolicy, type EngineExecutionLimitsPolicyConfig } from "./policies";
|
|
23
25
|
export { InMemoryBinaryStorage, InMemoryRunDataFactory } from "./runStorage";
|
|
@@ -66,7 +66,6 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
66
66
|
const resolvedParent = request.parent ?? state.parent;
|
|
67
67
|
const data = this.runDataFactory.create(state.outputsByNode);
|
|
68
68
|
const limits = this.resolveEngineLimitsFromState(state);
|
|
69
|
-
const persistedInput = pendingExecution.inputsByPort.in ?? request.input;
|
|
70
69
|
const base = this.runExecutionContextFactory.create({
|
|
71
70
|
runId: state.runId,
|
|
72
71
|
workflowId: state.workflowId,
|
|
@@ -78,21 +77,43 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
78
77
|
data,
|
|
79
78
|
nodeState: this.nodeStatePublisherFactory.create(state.runId, state.workflowId, resolvedParent),
|
|
80
79
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
80
|
+
|
|
81
|
+
const inputsByPort = pendingExecution.inputsByPort;
|
|
82
|
+
const portKeys = Object.keys(inputsByPort);
|
|
83
|
+
const kind = portKeys.length === 1 && portKeys[0] === "in" ? ("single" as const) : ("multi" as const);
|
|
84
|
+
const batchId = pendingExecution.batchId ?? "batch_1";
|
|
85
|
+
const activationRequest =
|
|
86
|
+
kind === "multi"
|
|
87
|
+
? this.nodeActivationRequestComposer.createMultiFromDefinitionWithActivation({
|
|
88
|
+
activationId: request.activationId,
|
|
89
|
+
runId: request.runId,
|
|
90
|
+
workflowId: request.workflowId,
|
|
91
|
+
parent: resolvedParent,
|
|
92
|
+
executionOptions: request.executionOptions ?? state.executionOptions,
|
|
93
|
+
base,
|
|
94
|
+
data,
|
|
95
|
+
definition: {
|
|
96
|
+
id: definition.id,
|
|
97
|
+
config: definition.config,
|
|
98
|
+
},
|
|
99
|
+
batchId,
|
|
100
|
+
inputsByPort,
|
|
101
|
+
})
|
|
102
|
+
: this.nodeActivationRequestComposer.createSingleFromDefinitionWithActivation({
|
|
103
|
+
activationId: request.activationId,
|
|
104
|
+
runId: request.runId,
|
|
105
|
+
workflowId: request.workflowId,
|
|
106
|
+
parent: resolvedParent,
|
|
107
|
+
executionOptions: request.executionOptions ?? state.executionOptions,
|
|
108
|
+
base,
|
|
109
|
+
data,
|
|
110
|
+
definition: {
|
|
111
|
+
id: definition.id,
|
|
112
|
+
config: definition.config,
|
|
113
|
+
},
|
|
114
|
+
batchId,
|
|
115
|
+
input: inputsByPort.in ?? request.input ?? [],
|
|
116
|
+
});
|
|
96
117
|
|
|
97
118
|
await this.continuation.markNodeRunning({
|
|
98
119
|
runId: activationRequest.runId,
|
|
@@ -131,7 +152,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
private async resumeAfterExecutionResult(
|
|
134
|
-
request:
|
|
155
|
+
request: Readonly<{ runId: string; activationId: string; nodeId: string }>,
|
|
135
156
|
outputs: unknown,
|
|
136
157
|
): Promise<void> {
|
|
137
158
|
try {
|
|
@@ -147,7 +168,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
147
168
|
}
|
|
148
169
|
|
|
149
170
|
private async resumeAfterExecutionError(
|
|
150
|
-
request:
|
|
171
|
+
request: Readonly<{ runId: string; activationId: string; nodeId: string }>,
|
|
151
172
|
error: Error,
|
|
152
173
|
): Promise<void> {
|
|
153
174
|
try {
|
|
@@ -335,7 +335,7 @@ export class RunContinuationService {
|
|
|
335
335
|
nextNodeId: next.nodeId,
|
|
336
336
|
request,
|
|
337
337
|
completedSnapshot,
|
|
338
|
-
nextNodeSnapshotsByNodeId,
|
|
338
|
+
nextNodeSnapshotsByNodeId: nextNodeSnapshotsByNodeId,
|
|
339
339
|
outputsByNode: data.dump(),
|
|
340
340
|
engineCounters,
|
|
341
341
|
error,
|
|
@@ -653,6 +653,12 @@ export class RunContinuationService {
|
|
|
653
653
|
planner.applyOutputs(queue, { fromNodeId: args.args.nodeId, outputs: triggerOutputs as any, batchId });
|
|
654
654
|
const next = planner.nextActivation(queue);
|
|
655
655
|
|
|
656
|
+
const finishedAt = completedSnapshot.finishedAt ?? completedSnapshot.updatedAt;
|
|
657
|
+
const mergedSnapshots = {
|
|
658
|
+
...(args.state.nodeSnapshotsByNodeId ?? {}),
|
|
659
|
+
[args.args.nodeId]: completedSnapshot,
|
|
660
|
+
};
|
|
661
|
+
|
|
656
662
|
if (!next) {
|
|
657
663
|
const lastNodeId = WorkflowExecutableNodeClassifierFactory.create(
|
|
658
664
|
args.workflow,
|
|
@@ -664,11 +670,8 @@ export class RunContinuationService {
|
|
|
664
670
|
status: "completed",
|
|
665
671
|
queue: [],
|
|
666
672
|
outputsByNode: data.dump(),
|
|
667
|
-
nodeSnapshotsByNodeId:
|
|
668
|
-
|
|
669
|
-
[args.args.nodeId]: completedSnapshot,
|
|
670
|
-
},
|
|
671
|
-
finishedAtIso: completedSnapshot.finishedAt ?? completedSnapshot.updatedAt,
|
|
673
|
+
nodeSnapshotsByNodeId: mergedSnapshots,
|
|
674
|
+
finishedAtIso: finishedAt,
|
|
672
675
|
});
|
|
673
676
|
await this.workflowExecutionRepository.save(completedState);
|
|
674
677
|
await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
|
|
@@ -705,11 +708,8 @@ export class RunContinuationService {
|
|
|
705
708
|
status: "failed",
|
|
706
709
|
queue: queue.map((q) => ({ ...q })),
|
|
707
710
|
outputsByNode: data.dump(),
|
|
708
|
-
nodeSnapshotsByNodeId:
|
|
709
|
-
|
|
710
|
-
[args.args.nodeId]: completedSnapshot,
|
|
711
|
-
},
|
|
712
|
-
finishedAtIso: completedSnapshot.finishedAt ?? completedSnapshot.updatedAt,
|
|
711
|
+
nodeSnapshotsByNodeId: mergedSnapshots,
|
|
712
|
+
finishedAtIso: finishedAt,
|
|
713
713
|
});
|
|
714
714
|
await this.workflowExecutionRepository.save(failedState);
|
|
715
715
|
await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
|
|
@@ -765,11 +765,6 @@ export class RunContinuationService {
|
|
|
765
765
|
nodeDefinition: nextDefinition,
|
|
766
766
|
});
|
|
767
767
|
|
|
768
|
-
const mergedSnapshots = {
|
|
769
|
-
...(args.state.nodeSnapshotsByNodeId ?? {}),
|
|
770
|
-
[args.args.nodeId]: completedSnapshot,
|
|
771
|
-
};
|
|
772
|
-
|
|
773
768
|
try {
|
|
774
769
|
const { queuedSnapshot, result } = await this.activationEnqueueService.enqueueActivationWithSnapshot({
|
|
775
770
|
runId: args.state.runId,
|
|
@@ -800,7 +795,6 @@ export class RunContinuationService {
|
|
|
800
795
|
return result;
|
|
801
796
|
} catch (cause) {
|
|
802
797
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
803
|
-
const finishedAt = completedSnapshot.finishedAt ?? completedSnapshot.updatedAt;
|
|
804
798
|
const result = await this.terminateRunAfterActivationEnqueueRejected({
|
|
805
799
|
wf: args.workflow,
|
|
806
800
|
state: args.state,
|
|
@@ -200,7 +200,7 @@ export class CurrentStateFrontierPlanner {
|
|
|
200
200
|
continue;
|
|
201
201
|
}
|
|
202
202
|
const incomingEdges = this.topology.incomingByNode.get(nodeId) ?? [];
|
|
203
|
-
const isFrontier = incomingEdges.every((edge) => this.isEdgeSatisfied(currentState, nodeId, edge.
|
|
203
|
+
const isFrontier = incomingEdges.every((edge) => this.isEdgeSatisfied(currentState, nodeId, edge.collectKey));
|
|
204
204
|
if (isFrontier) {
|
|
205
205
|
frontierNodeIds.push(nodeId);
|
|
206
206
|
}
|
|
@@ -235,7 +235,7 @@ export class CurrentStateFrontierPlanner {
|
|
|
235
235
|
requiredNodeIds.add(nodeId);
|
|
236
236
|
for (const edge of this.topology.incomingByNode.get(nodeId) ?? []) {
|
|
237
237
|
if (
|
|
238
|
-
!this.isEdgeSatisfied(currentState, nodeId, edge.
|
|
238
|
+
!this.isEdgeSatisfied(currentState, nodeId, edge.collectKey) ||
|
|
239
239
|
this.isNodeSatisfiedByOutputsOnly(currentState, edge.from.nodeId)
|
|
240
240
|
) {
|
|
241
241
|
this.collectRequiredNode(requiredNodeIds, currentState, edge.from.nodeId);
|
|
@@ -249,7 +249,7 @@ export class CurrentStateFrontierPlanner {
|
|
|
249
249
|
return [];
|
|
250
250
|
}
|
|
251
251
|
const expectedInputs = this.topology.expectedInputsByNode.get(nodeId) ?? [];
|
|
252
|
-
const usesCollect =
|
|
252
|
+
const usesCollect = this.usesCollect(nodeId);
|
|
253
253
|
if (usesCollect) {
|
|
254
254
|
const received: Record<InputPortKey, Items> = {};
|
|
255
255
|
for (const input of expectedInputs) {
|
|
@@ -268,7 +268,7 @@ export class CurrentStateFrontierPlanner {
|
|
|
268
268
|
];
|
|
269
269
|
}
|
|
270
270
|
const input = expectedInputs[0] ?? "in";
|
|
271
|
-
const incomingEdge = incomingEdges.find((edge) => edge.
|
|
271
|
+
const incomingEdge = incomingEdges.find((edge) => edge.collectKey === input);
|
|
272
272
|
return [
|
|
273
273
|
{
|
|
274
274
|
nodeId,
|
|
@@ -298,26 +298,31 @@ export class CurrentStateFrontierPlanner {
|
|
|
298
298
|
return this.hasOutputs(currentState, nodeId) && !this.hasCompletedSnapshot(currentState, nodeId);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
private isEdgeSatisfied(currentState: RunCurrentState, nodeId: NodeId,
|
|
302
|
-
const incomingEdge = (this.topology.incomingByNode.get(nodeId) ?? []).find(
|
|
301
|
+
private isEdgeSatisfied(currentState: RunCurrentState, nodeId: NodeId, collectKey: InputPortKey): boolean {
|
|
302
|
+
const incomingEdge = (this.topology.incomingByNode.get(nodeId) ?? []).find(
|
|
303
|
+
(edge) => edge.collectKey === collectKey,
|
|
304
|
+
);
|
|
303
305
|
if (!incomingEdge) {
|
|
304
306
|
return false;
|
|
305
307
|
}
|
|
306
|
-
|
|
308
|
+
const fromNodeId = incomingEdge.from.nodeId;
|
|
309
|
+
if (!this.isNodeSatisfied(currentState, fromNodeId)) {
|
|
307
310
|
return false;
|
|
308
311
|
}
|
|
309
312
|
if (this.usesCollect(nodeId)) {
|
|
310
313
|
return true;
|
|
311
314
|
}
|
|
312
|
-
const items = this.resolveOutputItems(currentState,
|
|
315
|
+
const items = this.resolveOutputItems(currentState, fromNodeId, incomingEdge.from.output);
|
|
313
316
|
if (items.length > 0) {
|
|
314
317
|
return true;
|
|
315
318
|
}
|
|
316
|
-
return this.shouldContinueAfterEmptyOutputFromSource(
|
|
319
|
+
return this.shouldContinueAfterEmptyOutputFromSource(fromNodeId);
|
|
317
320
|
}
|
|
318
321
|
|
|
319
|
-
private resolveInput(currentState: RunCurrentState, nodeId: NodeId,
|
|
320
|
-
const incomingEdge = (this.topology.incomingByNode.get(nodeId) ?? []).find(
|
|
322
|
+
private resolveInput(currentState: RunCurrentState, nodeId: NodeId, collectKey: InputPortKey): Items {
|
|
323
|
+
const incomingEdge = (this.topology.incomingByNode.get(nodeId) ?? []).find(
|
|
324
|
+
(edge) => edge.collectKey === collectKey,
|
|
325
|
+
);
|
|
321
326
|
if (!incomingEdge) {
|
|
322
327
|
return [];
|
|
323
328
|
}
|
|
@@ -333,21 +338,16 @@ export class CurrentStateFrontierPlanner {
|
|
|
333
338
|
return snapshot?.status === "completed" || snapshot?.status === "skipped";
|
|
334
339
|
}
|
|
335
340
|
|
|
336
|
-
private hasOutputPort(currentState: RunCurrentState, nodeId: NodeId, output: OutputPortKey): boolean {
|
|
337
|
-
const outputs = currentState.outputsByNode[nodeId];
|
|
338
|
-
if (!outputs) {
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
return Object.prototype.hasOwnProperty.call(outputs, output);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
341
|
private resolveOutputItems(currentState: RunCurrentState, nodeId: NodeId, output: OutputPortKey): Items {
|
|
345
342
|
return currentState.outputsByNode[nodeId]?.[output] ?? [];
|
|
346
343
|
}
|
|
347
344
|
|
|
348
345
|
private usesCollect(nodeId: NodeId): boolean {
|
|
349
346
|
const expectedInputs = this.topology.expectedInputsByNode.get(nodeId) ?? [];
|
|
350
|
-
|
|
347
|
+
if (expectedInputs.length !== 1 || expectedInputs[0] !== "in") {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
return (this.topology.incomingByNode.get(nodeId) ?? []).length > 1;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
private shouldContinueAfterEmptyOutputFromSource(nodeId: NodeId): boolean {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { InputPortKey, Items, NodeId, OutputPortKey, RunQueueEntry } from "../types";
|
|
2
2
|
|
|
3
3
|
import { WorkflowTopology } from "./WorkflowTopologyPlanner";
|
|
4
|
+
import type { TopologyOutgoingEdge } from "./WorkflowTopologyPlanner";
|
|
4
5
|
|
|
5
6
|
export type PlannedActivation =
|
|
6
7
|
| Readonly<{ kind: "single"; nodeId: NodeId; input: Items; batchId: string }>
|
|
@@ -25,9 +26,9 @@ export class RunQueuePlanner {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const inst = this.nodeInstances.get(toNodeId);
|
|
28
|
-
if (!this.isMultiInputNode(inst)) {
|
|
29
|
+
if (!this.isMultiInputNode(inst) && !this.supportsEngineFanInMerge(inst)) {
|
|
29
30
|
throw new Error(
|
|
30
|
-
`Node ${toNodeId} has ${inputs.length} inbound edges
|
|
31
|
+
`Node ${toNodeId} has ${inputs.length} inbound edges but does not support multi-input execution.`,
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -39,7 +40,7 @@ export class RunQueuePlanner {
|
|
|
39
40
|
if (e.output !== "main") continue;
|
|
40
41
|
this.enqueueEdge(queue, {
|
|
41
42
|
batchId: args.batchId,
|
|
42
|
-
to: e
|
|
43
|
+
to: this.toEnqueueTarget(e),
|
|
43
44
|
from: { nodeId: args.startNodeId, output: "main" },
|
|
44
45
|
items: args.items,
|
|
45
46
|
});
|
|
@@ -55,7 +56,7 @@ export class RunQueuePlanner {
|
|
|
55
56
|
const outItems = (args.outputs as any)[e.output] ?? [];
|
|
56
57
|
this.enqueueEdge(queue, {
|
|
57
58
|
batchId: args.batchId,
|
|
58
|
-
to: e
|
|
59
|
+
to: this.toEnqueueTarget(e),
|
|
59
60
|
from: { nodeId: args.fromNodeId, output: e.output },
|
|
60
61
|
items: outItems,
|
|
61
62
|
});
|
|
@@ -157,40 +158,54 @@ export class RunQueuePlanner {
|
|
|
157
158
|
*/
|
|
158
159
|
private usesTopologyCollectMerge(toNodeId: NodeId): boolean {
|
|
159
160
|
const expectedInputs = this.topology.expectedInputsByNode.get(toNodeId) ?? [];
|
|
160
|
-
|
|
161
|
+
if (expectedInputs.length !== 1 || expectedInputs[0] !== "in") {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return (this.topology.incomingByNode.get(toNodeId) ?? []).length > 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private toEnqueueTarget(edge: TopologyOutgoingEdge): Readonly<{
|
|
168
|
+
nodeId: NodeId;
|
|
169
|
+
input: InputPortKey;
|
|
170
|
+
collectKey: InputPortKey;
|
|
171
|
+
}> {
|
|
172
|
+
return edge.to;
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
private enqueueEdge(
|
|
164
176
|
queue: RunQueueEntry[],
|
|
165
177
|
args: Readonly<{
|
|
166
178
|
batchId: string;
|
|
167
|
-
to: { nodeId: NodeId; input: InputPortKey };
|
|
179
|
+
to: { nodeId: NodeId; input: InputPortKey; collectKey: InputPortKey };
|
|
168
180
|
from: { nodeId: NodeId; output: OutputPortKey };
|
|
169
181
|
items: Items;
|
|
170
182
|
}>,
|
|
183
|
+
emptyPathSourceNodeId?: NodeId,
|
|
171
184
|
): void {
|
|
172
185
|
const target = this.nodeInstances.get(args.to.nodeId);
|
|
173
186
|
const isMulti = this.usesTopologyCollectMerge(args.to.nodeId) || this.isMultiInputNode(target);
|
|
174
187
|
|
|
175
188
|
if (!isMulti) {
|
|
176
189
|
if (args.items.length === 0) {
|
|
177
|
-
|
|
190
|
+
const continueSourceNodeId = emptyPathSourceNodeId ?? args.from.nodeId;
|
|
191
|
+
if (this.shouldContinueAfterEmptyOutputFromSource(continueSourceNodeId)) {
|
|
178
192
|
queue.push({
|
|
179
193
|
nodeId: args.to.nodeId,
|
|
180
194
|
input: args.items,
|
|
181
|
-
toInput: args.to.
|
|
195
|
+
toInput: args.to.collectKey,
|
|
182
196
|
batchId: args.batchId,
|
|
183
197
|
from: args.from,
|
|
184
198
|
});
|
|
185
199
|
return;
|
|
186
200
|
}
|
|
187
|
-
|
|
201
|
+
const source = emptyPathSourceNodeId ?? args.from.nodeId;
|
|
202
|
+
this.propagateEmptyPath(queue, args.to.nodeId, args.batchId, source);
|
|
188
203
|
return;
|
|
189
204
|
}
|
|
190
205
|
queue.push({
|
|
191
206
|
nodeId: args.to.nodeId,
|
|
192
207
|
input: args.items,
|
|
193
|
-
toInput: args.to.
|
|
208
|
+
toInput: args.to.collectKey,
|
|
194
209
|
batchId: args.batchId,
|
|
195
210
|
from: args.from,
|
|
196
211
|
});
|
|
@@ -212,7 +227,7 @@ export class RunQueuePlanner {
|
|
|
212
227
|
}
|
|
213
228
|
|
|
214
229
|
const received = (collect.collect as any).received as Record<InputPortKey, Items>;
|
|
215
|
-
received[args.to.
|
|
230
|
+
received[args.to.collectKey] = args.items;
|
|
216
231
|
}
|
|
217
232
|
|
|
218
233
|
private shouldContinueAfterEmptyOutputFromSource(fromNodeId: NodeId): boolean {
|
|
@@ -223,14 +238,23 @@ export class RunQueuePlanner {
|
|
|
223
238
|
return def.config.continueWhenEmptyOutput === true;
|
|
224
239
|
}
|
|
225
240
|
|
|
226
|
-
private propagateEmptyPath(
|
|
241
|
+
private propagateEmptyPath(
|
|
242
|
+
queue: RunQueueEntry[],
|
|
243
|
+
nodeId: NodeId,
|
|
244
|
+
batchId: string,
|
|
245
|
+
emptyPathSourceNodeId: NodeId,
|
|
246
|
+
): void {
|
|
227
247
|
for (const edge of this.topology.outgoingByNode.get(nodeId) ?? []) {
|
|
228
|
-
this.enqueueEdge(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
248
|
+
this.enqueueEdge(
|
|
249
|
+
queue,
|
|
250
|
+
{
|
|
251
|
+
batchId,
|
|
252
|
+
to: edge.to,
|
|
253
|
+
from: { nodeId, output: edge.output },
|
|
254
|
+
items: [],
|
|
255
|
+
},
|
|
256
|
+
emptyPathSourceNodeId,
|
|
257
|
+
);
|
|
234
258
|
}
|
|
235
259
|
}
|
|
236
260
|
|
|
@@ -238,6 +262,19 @@ export class RunQueuePlanner {
|
|
|
238
262
|
return typeof (n as any)?.executeMulti === "function";
|
|
239
263
|
}
|
|
240
264
|
|
|
265
|
+
private hasRunnableExecute(n: unknown): boolean {
|
|
266
|
+
return (
|
|
267
|
+
typeof n === "object" &&
|
|
268
|
+
n !== null &&
|
|
269
|
+
(n as { kind?: string }).kind === "node" &&
|
|
270
|
+
typeof (n as { execute?: unknown }).execute === "function"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private supportsEngineFanInMerge(n: unknown): boolean {
|
|
275
|
+
return this.hasRunnableExecute(n) && !this.isMultiInputNode(n);
|
|
276
|
+
}
|
|
277
|
+
|
|
241
278
|
private describeUnsatisfiedCollect(queueEntry: RunQueueEntry): string {
|
|
242
279
|
const batchId = queueEntry.batchId ?? "batch_1";
|
|
243
280
|
const expectedInputs = queueEntry.collect?.expectedInputs ?? [];
|
|
@@ -287,7 +324,7 @@ export class RunQueuePlanner {
|
|
|
287
324
|
const matches: string[] = [];
|
|
288
325
|
for (const [sourceNodeId, edges] of this.topology.outgoingByNode.entries()) {
|
|
289
326
|
for (const edge of edges) {
|
|
290
|
-
if (edge.to.nodeId === nodeId && edge.to.
|
|
327
|
+
if (edge.to.nodeId === nodeId && edge.to.collectKey === input) {
|
|
291
328
|
matches.push(this.formatNodeLabel(sourceNodeId));
|
|
292
329
|
}
|
|
293
330
|
}
|
|
@@ -3,17 +3,22 @@ import { WorkflowExecutableNodeClassifierFactory } from "../workflow/definition/
|
|
|
3
3
|
|
|
4
4
|
type NodeDef = WorkflowDefinition["nodes"][number];
|
|
5
5
|
|
|
6
|
+
export type TopologyIncomingEdge = Readonly<{
|
|
7
|
+
from: Readonly<{ nodeId: NodeId; output: OutputPortKey }>;
|
|
8
|
+
input: InputPortKey;
|
|
9
|
+
collectKey: InputPortKey;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export type TopologyOutgoingEdge = Readonly<{
|
|
13
|
+
output: OutputPortKey;
|
|
14
|
+
to: Readonly<{ nodeId: NodeId; input: InputPortKey; collectKey: InputPortKey }>;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
6
17
|
export class WorkflowTopology {
|
|
7
18
|
private constructor(
|
|
8
19
|
public readonly defsById: ReadonlyMap<NodeId, NodeDef>,
|
|
9
|
-
public readonly outgoingByNode: ReadonlyMap<
|
|
10
|
-
|
|
11
|
-
ReadonlyArray<Readonly<{ output: OutputPortKey; to: Readonly<{ nodeId: NodeId; input: InputPortKey }> }>>
|
|
12
|
-
>,
|
|
13
|
-
public readonly incomingByNode: ReadonlyMap<
|
|
14
|
-
NodeId,
|
|
15
|
-
ReadonlyArray<Readonly<{ from: Readonly<{ nodeId: NodeId; output: OutputPortKey }>; input: InputPortKey }>>
|
|
16
|
-
>,
|
|
20
|
+
public readonly outgoingByNode: ReadonlyMap<NodeId, ReadonlyArray<TopologyOutgoingEdge>>,
|
|
21
|
+
public readonly incomingByNode: ReadonlyMap<NodeId, ReadonlyArray<TopologyIncomingEdge>>,
|
|
17
22
|
public readonly expectedInputsByNode: ReadonlyMap<NodeId, ReadonlyArray<InputPortKey>>,
|
|
18
23
|
public readonly rootNodeIds: ReadonlyArray<NodeId>,
|
|
19
24
|
) {}
|
|
@@ -25,46 +30,65 @@ export class WorkflowTopology {
|
|
|
25
30
|
if (classifier.isExecutableNodeId(n.id)) defs.set(n.id, n);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
const
|
|
29
|
-
NodeId,
|
|
30
|
-
Array<Readonly<{ output: OutputPortKey; to: Readonly<{ nodeId: NodeId; input: InputPortKey }> }>>
|
|
31
|
-
>();
|
|
33
|
+
const incomingByNode = new Map<NodeId, TopologyIncomingEdge[]>();
|
|
32
34
|
for (const e of wf.edges) {
|
|
33
35
|
if (!classifier.isExecutableNodeId(e.from.nodeId) || !classifier.isExecutableNodeId(e.to.nodeId)) {
|
|
34
36
|
continue;
|
|
35
37
|
}
|
|
36
|
-
const list =
|
|
37
|
-
list.push({
|
|
38
|
-
|
|
38
|
+
const list = incomingByNode.get(e.to.nodeId) ?? [];
|
|
39
|
+
list.push({
|
|
40
|
+
from: { nodeId: e.from.nodeId, output: e.from.output },
|
|
41
|
+
input: e.to.input,
|
|
42
|
+
collectKey: e.to.input,
|
|
43
|
+
});
|
|
44
|
+
incomingByNode.set(e.to.nodeId, list);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const duplicateInputCounts = new Map<NodeId, Map<InputPortKey, number>>();
|
|
48
|
+
for (const [toNodeId, edges] of incomingByNode.entries()) {
|
|
49
|
+
const counts = new Map<InputPortKey, number>();
|
|
50
|
+
for (const edge of edges) {
|
|
51
|
+
counts.set(edge.input, (counts.get(edge.input) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
duplicateInputCounts.set(toNodeId, counts);
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
for (const [toNodeId, edges] of incomingByNode.entries()) {
|
|
57
|
+
const counts = duplicateInputCounts.get(toNodeId) ?? new Map();
|
|
58
|
+
for (let i = 0; i < edges.length; i++) {
|
|
59
|
+
const edge = edges[i]!;
|
|
60
|
+
const dup = (counts.get(edge.input) ?? 0) > 1;
|
|
61
|
+
const collectKey = dup ? `${edge.from.nodeId}:${edge.from.output}` : edge.input;
|
|
62
|
+
edges[i] = { ...edge, collectKey };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const outgoing = new Map<NodeId, TopologyOutgoingEdge[]>();
|
|
45
67
|
for (const e of wf.edges) {
|
|
46
68
|
if (!classifier.isExecutableNodeId(e.from.nodeId) || !classifier.isExecutableNodeId(e.to.nodeId)) {
|
|
47
69
|
continue;
|
|
48
70
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
const counts = duplicateInputCounts.get(e.to.nodeId) ?? new Map();
|
|
72
|
+
const dup = (counts.get(e.to.input) ?? 0) > 1;
|
|
73
|
+
const collectKey = dup ? `${e.from.nodeId}:${e.from.output}` : e.to.input;
|
|
74
|
+
const list = outgoing.get(e.from.nodeId) ?? [];
|
|
75
|
+
list.push({
|
|
76
|
+
output: e.from.output,
|
|
77
|
+
to: { nodeId: e.to.nodeId, input: e.to.input, collectKey },
|
|
78
|
+
});
|
|
79
|
+
outgoing.set(e.from.nodeId, list);
|
|
52
80
|
}
|
|
53
81
|
|
|
54
82
|
const expected = new Map<NodeId, InputPortKey[]>();
|
|
55
|
-
for (const [toNodeId,
|
|
56
|
-
const counts = new Map<InputPortKey, number>();
|
|
57
|
-
for (const edge of inputs) counts.set(edge.input, (counts.get(edge.input) ?? 0) + 1);
|
|
58
|
-
for (const [k, n] of counts.entries()) {
|
|
59
|
-
if (n > 1) throw new Error(`Node ${toNodeId} has multiple edges into input '${k}'. Use a Merge node upstream.`);
|
|
60
|
-
}
|
|
61
|
-
|
|
83
|
+
for (const [toNodeId, edges] of incomingByNode.entries()) {
|
|
62
84
|
const order: InputPortKey[] = [];
|
|
63
85
|
const seen = new Set<InputPortKey>();
|
|
64
|
-
for (const edge of
|
|
65
|
-
if (seen.has(edge.
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
for (const edge of edges) {
|
|
87
|
+
if (seen.has(edge.collectKey)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
seen.add(edge.collectKey);
|
|
91
|
+
order.push(edge.collectKey);
|
|
68
92
|
}
|
|
69
93
|
expected.set(toNodeId, order);
|
|
70
94
|
}
|