@codemation/core 0.3.0 → 0.5.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 +23 -0
- package/dist/{EngineRuntimeRegistration.types-Bjeo7Sfq.d.ts → EngineRuntimeRegistration.types-BtTZolK0.d.ts} +2 -2
- package/dist/{EngineWorkflowRunnerService-Dd4yD31l.d.cts → EngineWorkflowRunnerService-Ddl0fekp.d.cts} +2 -2
- package/dist/{InMemoryRunDataFactory-OUzDmAHt.d.cts → InMemoryRunDataFactory-i-u2yngD.d.cts} +11 -3
- package/dist/{RunIntentService-Bkg4oYrM.d.cts → RunIntentService-Cjx-glgz.d.cts} +232 -237
- package/dist/{RunIntentService-BAKikN8h.d.ts → RunIntentService-Dkr4YwN8.d.ts} +313 -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-B2v4wtys.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/credentialTypes.ts +9 -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
|
@@ -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
|
}
|
|
@@ -1,24 +1,18 @@
|
|
|
1
|
-
import type { Item,
|
|
1
|
+
import type { Item, RunnableNode, RunnableNodeExecuteArgs } from "../types";
|
|
2
2
|
|
|
3
3
|
import type { ItemHarnessNodeConfig } from "./ItemHarnessNodeConfig";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Item-mode harness node for engine tests (see {@link ItemHarnessNodeConfig}).
|
|
7
7
|
*/
|
|
8
|
-
export class ItemHarnessNode implements
|
|
8
|
+
export class ItemHarnessNode implements RunnableNode<ItemHarnessNodeConfig<any, any>> {
|
|
9
9
|
readonly kind = "node" as const;
|
|
10
10
|
readonly outputPorts = ["main"] as const;
|
|
11
11
|
|
|
12
|
-
async
|
|
13
|
-
input: unknown;
|
|
14
|
-
item: Item;
|
|
15
|
-
itemIndex: number;
|
|
16
|
-
items: Items;
|
|
17
|
-
ctx: NodeExecutionContext<ItemHarnessNodeConfig<any, any, any>>;
|
|
18
|
-
}): Promise<unknown> {
|
|
12
|
+
async execute(args: RunnableNodeExecuteArgs<ItemHarnessNodeConfig<any, any>>): Promise<unknown> {
|
|
19
13
|
return await args.ctx.config.runOne({
|
|
20
14
|
input: args.input as never,
|
|
21
|
-
item: args.item,
|
|
15
|
+
item: args.item as Item,
|
|
22
16
|
itemIndex: args.itemIndex,
|
|
23
17
|
items: args.items,
|
|
24
18
|
ctx: args.ctx,
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import type { ZodType } from "zod";
|
|
2
2
|
|
|
3
3
|
import type { TypeToken } from "../di";
|
|
4
|
-
import type { Item,
|
|
4
|
+
import type { Item, Items, NodeExecutionContext, RunnableNodeConfig } from "../types";
|
|
5
5
|
|
|
6
6
|
import { ItemHarnessNode } from "./ItemHarnessNode";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Item-mode harness node config for engine tests: engine applies {@link RunnableNodeConfig.inputSchema}
|
|
10
|
-
*
|
|
9
|
+
* Item-mode harness node config for engine tests: engine applies {@link RunnableNodeConfig.inputSchema},
|
|
10
|
+
* then {@link ItemHarnessNode.execute} per item.
|
|
11
11
|
*/
|
|
12
|
-
export class ItemHarnessNodeConfig<TIn = unknown, TOut = unknown
|
|
13
|
-
TIn,
|
|
14
|
-
TOut,
|
|
15
|
-
TWire
|
|
16
|
-
> {
|
|
12
|
+
export class ItemHarnessNodeConfig<TIn = unknown, TOut = unknown> implements RunnableNodeConfig<TIn, TOut> {
|
|
17
13
|
readonly kind = "node" as const;
|
|
18
14
|
readonly type: TypeToken<unknown> = ItemHarnessNode;
|
|
19
15
|
|
|
@@ -22,21 +18,16 @@ export class ItemHarnessNodeConfig<TIn = unknown, TOut = unknown, TWire = TIn> i
|
|
|
22
18
|
public readonly inputSchema: ZodType<TIn>,
|
|
23
19
|
public readonly runOne: (args: {
|
|
24
20
|
input: TIn;
|
|
25
|
-
item: Item<
|
|
21
|
+
item: Item<TIn>;
|
|
26
22
|
itemIndex: number;
|
|
27
|
-
items: Items<
|
|
28
|
-
ctx: NodeExecutionContext<ItemHarnessNodeConfig<TIn, TOut
|
|
23
|
+
items: Items<TIn>;
|
|
24
|
+
ctx: NodeExecutionContext<ItemHarnessNodeConfig<TIn, TOut>>;
|
|
29
25
|
}) => TOut | Promise<TOut>,
|
|
30
26
|
public readonly opts: Readonly<{
|
|
31
27
|
id?: string;
|
|
32
|
-
mapInput?: ItemInputMapper<TWire, TIn>;
|
|
33
28
|
}> = {},
|
|
34
29
|
) {}
|
|
35
30
|
|
|
36
|
-
get mapInput(): ItemInputMapper<TWire, TIn> | undefined {
|
|
37
|
-
return this.opts.mapInput;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
get id(): string | undefined {
|
|
41
32
|
return this.opts.id;
|
|
42
33
|
}
|
|
@@ -32,6 +32,7 @@ import type { RegistrarEngineTestKitHandle, RegistrarEngineTestKitOptions } from
|
|
|
32
32
|
import { ItemHarnessNode } from "./ItemHarnessNode";
|
|
33
33
|
import { SubWorkflowRunnerNode } from "./SubWorkflowRunnerTestNode";
|
|
34
34
|
import { WorkflowTestHarnessManualTriggerNode } from "./WorkflowTestHarnessManualTrigger";
|
|
35
|
+
import { SwitchHarnessNode } from "./SwitchHarnessNode";
|
|
35
36
|
|
|
36
37
|
export class RegistrarEngineTestKitFactory {
|
|
37
38
|
static create(options: RegistrarEngineTestKitOptions = {}): RegistrarEngineTestKitHandle {
|
|
@@ -110,6 +111,7 @@ export class RegistrarEngineTestKitFactory {
|
|
|
110
111
|
new SubWorkflowRunnerNode(workflowRunner as WorkflowRunnerService),
|
|
111
112
|
);
|
|
112
113
|
dependencyContainer.registerInstance(ItemHarnessNode, new ItemHarnessNode());
|
|
114
|
+
dependencyContainer.registerInstance(SwitchHarnessNode, new SwitchHarnessNode());
|
|
113
115
|
dependencyContainer.registerInstance(
|
|
114
116
|
WorkflowTestHarnessManualTriggerNode,
|
|
115
117
|
new WorkflowTestHarnessManualTriggerNode(),
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
/* eslint-disable codemation/single-class-per-file -- Runnable config and implementation share a TypeToken pairing. */
|
|
2
2
|
import type { WorkflowRunnerService } from "../contracts/runtimeTypes";
|
|
3
3
|
import type { TypeToken } from "../di";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
Items,
|
|
7
|
-
NodeExecutionContext,
|
|
8
|
-
NodeId,
|
|
9
|
-
NodeOutputs,
|
|
10
|
-
Node,
|
|
11
|
-
RunnableNodeConfig,
|
|
12
|
-
WorkflowId,
|
|
13
|
-
} from "../types";
|
|
4
|
+
import type { Item, NodeId, RunnableNode, RunnableNodeConfig, RunnableNodeExecuteArgs, WorkflowId } from "../types";
|
|
5
|
+
import { emitPorts } from "../contracts/emitPorts";
|
|
14
6
|
|
|
15
7
|
/**
|
|
16
8
|
* Test harness subworkflow runner (mirrors integration patterns; lives under {@link "@codemation/core/testing"}).
|
|
@@ -22,6 +14,9 @@ export class SubWorkflowRunnerConfig<TInputJson = unknown, TOutputJson = unknown
|
|
|
22
14
|
readonly kind = "node" as const;
|
|
23
15
|
readonly type: TypeToken<unknown> = SubWorkflowRunnerNode;
|
|
24
16
|
|
|
17
|
+
readonly workflowId: WorkflowId;
|
|
18
|
+
readonly startAt: NodeId | undefined;
|
|
19
|
+
|
|
25
20
|
constructor(
|
|
26
21
|
public readonly name: string,
|
|
27
22
|
public readonly args: Readonly<{
|
|
@@ -30,7 +25,10 @@ export class SubWorkflowRunnerConfig<TInputJson = unknown, TOutputJson = unknown
|
|
|
30
25
|
id?: string;
|
|
31
26
|
execution?: Readonly<{ hint?: "local" | "worker"; queue?: string }>;
|
|
32
27
|
}>,
|
|
33
|
-
) {
|
|
28
|
+
) {
|
|
29
|
+
this.workflowId = args.workflowId;
|
|
30
|
+
this.startAt = args.startAt;
|
|
31
|
+
}
|
|
34
32
|
|
|
35
33
|
get id(): string | undefined {
|
|
36
34
|
return this.args.id;
|
|
@@ -39,45 +37,32 @@ export class SubWorkflowRunnerConfig<TInputJson = unknown, TOutputJson = unknown
|
|
|
39
37
|
get execution(): Readonly<{ hint?: "local" | "worker"; queue?: string }> | undefined {
|
|
40
38
|
return this.args.execution;
|
|
41
39
|
}
|
|
42
|
-
|
|
43
|
-
get workflowId(): WorkflowId {
|
|
44
|
-
return this.args.workflowId;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
get startAt(): NodeId | undefined {
|
|
48
|
-
return this.args.startAt;
|
|
49
|
-
}
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
export class SubWorkflowRunnerNode implements
|
|
42
|
+
export class SubWorkflowRunnerNode implements RunnableNode<SubWorkflowRunnerConfig<any, any>> {
|
|
53
43
|
readonly kind = "node" as const;
|
|
54
44
|
readonly outputPorts = ["main"] as const;
|
|
55
45
|
|
|
56
46
|
constructor(private readonly workflows: WorkflowRunnerService) {}
|
|
57
47
|
|
|
58
|
-
async execute(
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
if (result.status !== "completed") {
|
|
76
|
-
throw new Error(`Subworkflow ${ctx.config.workflowId} did not complete (status=${result.status})`);
|
|
77
|
-
}
|
|
78
|
-
out.push(...result.outputs);
|
|
48
|
+
async execute(args: RunnableNodeExecuteArgs<SubWorkflowRunnerConfig<any, any>>): Promise<unknown> {
|
|
49
|
+
const current = args.item as Item;
|
|
50
|
+
const result = await this.workflows.runById({
|
|
51
|
+
workflowId: args.ctx.config.workflowId,
|
|
52
|
+
startAt: args.ctx.config.startAt,
|
|
53
|
+
items: [current],
|
|
54
|
+
parent: {
|
|
55
|
+
runId: args.ctx.runId,
|
|
56
|
+
workflowId: args.ctx.workflowId,
|
|
57
|
+
nodeId: args.ctx.nodeId,
|
|
58
|
+
subworkflowDepth: args.ctx.subworkflowDepth,
|
|
59
|
+
engineMaxNodeActivations: args.ctx.engineMaxNodeActivations,
|
|
60
|
+
engineMaxSubworkflowDepth: args.ctx.engineMaxSubworkflowDepth,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
if (result.status !== "completed") {
|
|
64
|
+
throw new Error(`Subworkflow ${args.ctx.config.workflowId} did not complete (status=${result.status})`);
|
|
79
65
|
}
|
|
80
|
-
|
|
81
|
-
return { main: out };
|
|
66
|
+
return emitPorts({ main: result.outputs });
|
|
82
67
|
}
|
|
83
68
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { TypeToken } from "../di";
|
|
2
|
+
import type {
|
|
3
|
+
Item,
|
|
4
|
+
Items,
|
|
5
|
+
NodeExecutionContext,
|
|
6
|
+
RunnableNode,
|
|
7
|
+
RunnableNodeConfig,
|
|
8
|
+
RunnableNodeExecuteArgs,
|
|
9
|
+
} from "../types";
|
|
10
|
+
import { emitPorts } from "../contracts/emitPorts";
|
|
11
|
+
|
|
12
|
+
export class SwitchHarnessNode implements RunnableNode<SwitchHarnessNodeConfig<any>> {
|
|
13
|
+
readonly kind = "node" as const;
|
|
14
|
+
readonly outputPorts = [] as const;
|
|
15
|
+
|
|
16
|
+
async execute(args: RunnableNodeExecuteArgs<SwitchHarnessNodeConfig<any>>): Promise<unknown> {
|
|
17
|
+
const key = await Promise.resolve(
|
|
18
|
+
args.ctx.config.cfg.resolveCaseKey(args.item as Item, args.itemIndex, args.items, args.ctx),
|
|
19
|
+
);
|
|
20
|
+
const { cases, defaultCase } = args.ctx.config.cfg;
|
|
21
|
+
const port = cases.includes(key) ? key : defaultCase;
|
|
22
|
+
return emitPorts({
|
|
23
|
+
[port]: [args.item],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SwitchHarnessNodeConfig<TInputJson = unknown> implements RunnableNodeConfig<TInputJson, TInputJson> {
|
|
29
|
+
readonly kind = "node" as const;
|
|
30
|
+
readonly type: TypeToken<unknown> = SwitchHarnessNode;
|
|
31
|
+
readonly lineageCarry = "carryThrough" as const;
|
|
32
|
+
readonly declaredOutputPorts: ReadonlyArray<string>;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
public readonly name: string,
|
|
36
|
+
public readonly cfg: Readonly<{
|
|
37
|
+
cases: readonly string[];
|
|
38
|
+
defaultCase: string;
|
|
39
|
+
resolveCaseKey: (
|
|
40
|
+
item: Item<TInputJson>,
|
|
41
|
+
index: number,
|
|
42
|
+
items: Items<TInputJson>,
|
|
43
|
+
ctx: NodeExecutionContext<SwitchHarnessNodeConfig<TInputJson>>,
|
|
44
|
+
) => string | Promise<string>;
|
|
45
|
+
}>,
|
|
46
|
+
public readonly opts: Readonly<{ id?: string }> = {},
|
|
47
|
+
) {
|
|
48
|
+
this.declaredOutputPorts = [...new Set([...cfg.cases, cfg.defaultCase])].sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get id(): string | undefined {
|
|
52
|
+
return this.opts.id;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/types/index.ts
CHANGED