@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/{EngineRuntimeRegistration.types-Bjeo7Sfq.d.ts → EngineRuntimeRegistration.types-BtTZolK0.d.ts} +2 -2
  3. package/dist/{EngineWorkflowRunnerService-Dd4yD31l.d.cts → EngineWorkflowRunnerService-Ddl0fekp.d.cts} +2 -2
  4. package/dist/{InMemoryRunDataFactory-OUzDmAHt.d.cts → InMemoryRunDataFactory-i-u2yngD.d.cts} +11 -3
  5. package/dist/{RunIntentService-Bkg4oYrM.d.cts → RunIntentService-Cjx-glgz.d.cts} +232 -237
  6. package/dist/{RunIntentService-BAKikN8h.d.ts → RunIntentService-Dkr4YwN8.d.ts} +313 -259
  7. package/dist/bootstrap/index.cjs +2 -2
  8. package/dist/bootstrap/index.d.cts +19 -7
  9. package/dist/bootstrap/index.d.ts +3 -3
  10. package/dist/bootstrap/index.js +2 -2
  11. package/dist/{bootstrap-DwS5S7s9.cjs → bootstrap-DHH2uo-W.cjs} +4 -2
  12. package/dist/bootstrap-DHH2uo-W.cjs.map +1 -0
  13. package/dist/{bootstrap-BD6CobHl.js → bootstrap-DbUlOl11.js} +4 -2
  14. package/dist/bootstrap-DbUlOl11.js.map +1 -0
  15. package/dist/{index-uCm9l0nw.d.ts → index-B2v4wtys.d.ts} +62 -34
  16. package/dist/index.cjs +22 -15
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +108 -42
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +13 -16
  21. package/dist/index.js.map +1 -1
  22. package/dist/{runtime-Cy-3FTI_.js → runtime-BdH94eBR.js} +502 -123
  23. package/dist/runtime-BdH94eBR.js.map +1 -0
  24. package/dist/{runtime-ZJUpWmPH.cjs → runtime-feFn8OmG.cjs} +561 -122
  25. package/dist/runtime-feFn8OmG.cjs.map +1 -0
  26. package/dist/testing.cjs +40 -36
  27. package/dist/testing.cjs.map +1 -1
  28. package/dist/testing.d.cts +17 -26
  29. package/dist/testing.d.ts +17 -26
  30. package/dist/testing.js +40 -36
  31. package/dist/testing.js.map +1 -1
  32. package/dist/{workflowActivationPolicy-BzyzXLa_.cjs → workflowActivationPolicy-6V3OJD3N.cjs} +65 -19
  33. package/dist/workflowActivationPolicy-6V3OJD3N.cjs.map +1 -0
  34. package/dist/{workflowActivationPolicy-B8HzTk3o.js → workflowActivationPolicy-Td9HTOuD.js} +65 -19
  35. package/dist/workflowActivationPolicy-Td9HTOuD.js.map +1 -0
  36. package/package.json +2 -1
  37. package/src/ai/AgentConfigInspectorFactory.ts +4 -0
  38. package/src/ai/AgentMessageConfigNormalizerFactory.ts +7 -0
  39. package/src/ai/AgentToolFactory.ts +2 -2
  40. package/src/ai/AiHost.ts +11 -10
  41. package/src/ai/NodeBackedToolConfig.ts +1 -1
  42. package/src/authoring/defineNode.types.ts +48 -72
  43. package/src/authoring/index.ts +1 -1
  44. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +8 -0
  45. package/src/contracts/credentialTypes.ts +9 -0
  46. package/src/contracts/emitPorts.ts +27 -0
  47. package/src/contracts/index.ts +3 -0
  48. package/src/contracts/itemMeta.ts +11 -0
  49. package/src/contracts/itemValue.ts +147 -0
  50. package/src/contracts/runtimeTypes.ts +39 -22
  51. package/src/contracts/workflowTypes.ts +26 -56
  52. package/src/execution/FanInMergeByOriginMerger.ts +67 -0
  53. package/src/execution/ItemValueResolver.ts +27 -0
  54. package/src/execution/NodeActivationRequestComposer.ts +25 -0
  55. package/src/execution/NodeActivationRequestInputPreparer.ts +57 -25
  56. package/src/execution/NodeExecutor.ts +199 -30
  57. package/src/execution/NodeOutputNormalizer.ts +90 -0
  58. package/src/execution/index.ts +2 -0
  59. package/src/index.ts +2 -0
  60. package/src/orchestration/NodeExecutionRequestHandlerService.ts +39 -18
  61. package/src/orchestration/RunContinuationService.ts +11 -17
  62. package/src/planning/CurrentStateFrontierPlanner.ts +20 -20
  63. package/src/planning/RunQueuePlanner.ts +56 -19
  64. package/src/planning/WorkflowTopologyPlanner.ts +57 -33
  65. package/src/testing/ItemHarnessNode.ts +4 -10
  66. package/src/testing/ItemHarnessNodeConfig.ts +7 -16
  67. package/src/testing/RegistrarEngineTestKitFactory.ts +2 -0
  68. package/src/testing/SubWorkflowRunnerTestNode.ts +28 -43
  69. package/src/testing/SwitchHarnessNode.ts +54 -0
  70. package/src/types/index.ts +3 -0
  71. package/src/workflow/dsl/ChainCursorResolver.ts +68 -23
  72. package/src/workflow/dsl/WorkflowBuilder.ts +3 -5
  73. package/src/workflow/dsl/workflowBuilderTypes.ts +5 -8
  74. package/src/workflowSnapshots/MissingRuntimeNode.ts +4 -4
  75. package/src/workflowSnapshots/MissingRuntimeNodeConfig.ts +2 -2
  76. package/src/workflowSnapshots/WorkflowSnapshotCodec.ts +16 -7
  77. package/dist/bootstrap-BD6CobHl.js.map +0 -1
  78. package/dist/bootstrap-DwS5S7s9.cjs.map +0 -1
  79. package/dist/runtime-Cy-3FTI_.js.map +0 -1
  80. package/dist/runtime-ZJUpWmPH.cjs.map +0 -1
  81. package/dist/workflowActivationPolicy-B8HzTk3o.js.map +0 -1
  82. package/dist/workflowActivationPolicy-BzyzXLa_.cjs.map +0 -1
@@ -25,6 +25,7 @@ import type {
25
25
  NodeConfigBase,
26
26
  NodeId,
27
27
  NodeOutputs,
28
+ RunnableNodeConfig,
28
29
  OutputPortKey,
29
30
  ParentExecutionRef,
30
31
  RunDataFactory,
@@ -221,39 +222,55 @@ export interface EngineHost {
221
222
  workflows?: WorkflowRunnerService;
222
223
  }
223
224
 
224
- export interface Node<TConfig extends NodeConfigBase = NodeConfigBase> {
225
- kind: "node";
226
- outputPorts: ReadonlyArray<OutputPortKey>;
227
- execute(items: Items, ctx: NodeExecutionContext<TConfig>): Promise<NodeOutputs>;
228
- }
229
-
230
225
  /**
231
- * Single-input runnable node with per-item execution on `main` only (1→1 default).
232
- * Engine applies {@link RunnableNodeConfig.mapInput} (if any) + `inputSchema.parse` before `executeOne`.
226
+ * Per-item runnable node: return JSON, an array to fan-out on `main`, or {@link emitPorts} for multi-port emission.
227
+ * Engine applies `inputSchema.parse(item.json)` and passes the result as `args.input` (wire `item.json` is unchanged).
233
228
  */
234
- export interface ItemNode<
235
- TConfig extends NodeConfigBase = NodeConfigBase,
229
+ export interface RunnableNodeExecuteArgs<
230
+ TConfig extends RunnableNodeConfig<any, any> = RunnableNodeConfig<any, any>,
236
231
  TInputJson = unknown,
237
- TOutputJson = unknown,
232
+ > {
233
+ readonly input: TInputJson;
234
+ readonly item: Item;
235
+ readonly itemIndex: number;
236
+ readonly items: Items;
237
+ readonly ctx: NodeExecutionContext<TConfig>;
238
+ }
239
+
240
+ export interface RunnableNode<
241
+ TConfig extends RunnableNodeConfig<any, any> = RunnableNodeConfig<any, any>,
242
+ TInputJson = unknown,
243
+ _TOutputJson = unknown,
238
244
  > {
239
245
  readonly kind: "node";
240
- readonly outputPorts: readonly ["main"];
246
+ /**
247
+ * Declared output ports (e.g. `["main"]`).
248
+ *
249
+ * Prefer describing dynamic router ports (Switch) and fixed multi-ports (If true/false)
250
+ * via {@link NodeConfigBase.declaredOutputPorts}. Engine defaults to `["main"]` when omitted.
251
+ */
252
+ readonly outputPorts?: ReadonlyArray<OutputPortKey>;
241
253
  /** When omitted, engine uses {@link RunnableNodeConfig.inputSchema} or `z.unknown()`. */
242
254
  readonly inputSchema?: ZodType<TInputJson>;
243
- executeOne(
244
- args: Readonly<{
245
- input: TInputJson;
246
- item: Item;
247
- itemIndex: number;
248
- items: Items;
249
- ctx: NodeExecutionContext<TConfig>;
250
- }>,
251
- ): Promise<TOutputJson> | TOutputJson;
255
+ execute(args: RunnableNodeExecuteArgs<TConfig, TInputJson>): Promise<unknown> | unknown;
252
256
  }
253
257
 
258
+ /** @deprecated Use {@link RunnableNode} */
259
+ export type ItemNode<
260
+ TConfig extends RunnableNodeConfig<any, any> = RunnableNodeConfig<any, any>,
261
+ TInputJson = unknown,
262
+ TOutputJson = unknown,
263
+ > = RunnableNode<TConfig, TInputJson, TOutputJson>;
264
+
254
265
  export interface MultiInputNode<TConfig extends NodeConfigBase = NodeConfigBase> {
255
266
  kind: "node";
256
- outputPorts: ReadonlyArray<OutputPortKey>;
267
+ /**
268
+ * Declared output ports (typically `["main"]`).
269
+ *
270
+ * Prefer describing ports for authoring/canvas via {@link NodeConfigBase.declaredOutputPorts}.
271
+ * Engine defaults to `["main"]` when omitted.
272
+ */
273
+ outputPorts?: ReadonlyArray<OutputPortKey>;
257
274
  executeMulti(inputsByPort: NodeInputsByPort, ctx: NodeExecutionContext<TConfig>): Promise<NodeOutputs>;
258
275
  }
259
276
 
@@ -17,6 +17,8 @@ export interface JsonObject {
17
17
  }
18
18
  export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
19
19
  export type JsonArray = ReadonlyArray<JsonValue>;
20
+ /** JSON value that is not a top-level array (nested arrays inside objects are allowed). */
21
+ export type JsonNonArray = JsonPrimitive | JsonObject;
20
22
 
21
23
  export interface Edge {
22
24
  from: { nodeId: NodeId; output: OutputPortKey };
@@ -81,74 +83,45 @@ export interface NodeConfigBase {
81
83
  * main batches skip downstream execution and propagate the empty path.
82
84
  */
83
85
  readonly continueWhenEmptyOutput?: boolean;
86
+ /**
87
+ * Declared I/O port names for canvas authoring (unioned with ports inferred from edges).
88
+ * Use for dynamic routers (Switch) and future error ports.
89
+ */
90
+ readonly declaredOutputPorts?: ReadonlyArray<OutputPortKey>;
91
+ readonly declaredInputPorts?: ReadonlyArray<InputPortKey>;
84
92
  getCredentialRequirements?(): ReadonlyArray<CredentialRequirement>;
85
93
  }
86
94
 
87
95
  export declare const runnableNodeInputType: unique symbol;
88
96
  export declare const runnableNodeOutputType: unique symbol;
89
- /** Phantom: JSON shape on the wire from upstream before {@link RunnableNodeConfig.mapInput}. */
90
- export declare const runnableNodeWireType: unique symbol;
91
97
  export declare const triggerNodeOutputType: unique symbol;
92
98
 
93
- /**
94
- * Read-only execution slice passed to {@link RunnableNodeConfig.mapInput} (aligned with the engine’s
95
- * node execution context for `runId`, `data`, etc.). Use **`ctx.data`** to read **any completed** upstream
96
- * node’s outputs in this run (e.g. `ctx.data.getOutputItems(nodeIdA, "main")` while mapping at D), not only
97
- * the immediate predecessor’s {@link ItemInputMapperArgs.item}.
98
- */
99
- export interface ItemInputMapperContext {
100
- readonly runId: RunId;
101
- readonly workflowId: WorkflowId;
102
- /** Node whose activation is being prepared (the consumer of `mapInput`). */
103
- readonly nodeId: NodeId;
104
- readonly activationId: NodeActivationId;
105
- readonly parent?: ParentExecutionRef;
106
- readonly data: RunDataSnapshot;
107
- }
99
+ export type LineageCarryPolicy = "emitOnly" | "carryThrough";
108
100
 
109
101
  /**
110
- * Arguments for optional per-item input mapping applied by the engine before Zod validation.
102
+ * Runnable node: **`TInputJson`** is what **`inputSchema`** validates on **`item.json`** (the wire payload).
103
+ * **`TOutputJson`** is emitted `item.json` on outputs.
111
104
  */
112
- export interface ItemInputMapperArgs<TWireJson = unknown> {
113
- readonly item: Item<TWireJson>;
114
- readonly itemIndex: number;
115
- readonly items: Items<TWireJson>;
116
- readonly ctx: ItemInputMapperContext;
117
- }
118
-
119
- /**
120
- * Per-item mapper before Zod validation. Uses a **bivariant** method signature so concrete
121
- * `ItemInputMapper<SpecificWire, TIn>` remains assignable to `RunnableNodeConfig` fields typed as
122
- * `ItemInputMapper<unknown, unknown>` (same pattern as React-style callbacks).
123
- */
124
- export type ItemInputMapper<TWireJson = unknown, TInputJson = unknown> = {
125
- bivarianceHack(args: ItemInputMapperArgs<TWireJson>): TInputJson | Promise<TInputJson>;
126
- }["bivarianceHack"];
127
-
128
- /**
129
- * Runnable node: **`TInputJson`** is the payload after `mapInput` (if any) + Zod validation — what {@link ItemNode}
130
- * `executeOne` receives. **`TOutputJson`** is emitted `item.json` on outputs. **`TWireJson`** is `item.json` from
131
- * upstream **before** `mapInput`; it defaults to **`TInputJson`** when there is no mapper or wire differs from execute input.
132
- */
133
- export interface RunnableNodeConfig<
134
- TInputJson = unknown,
135
- TOutputJson = unknown,
136
- TWireJson = TInputJson,
137
- > extends NodeConfigBase {
105
+ export interface RunnableNodeConfig<TInputJson = unknown, TOutputJson = unknown> extends NodeConfigBase {
138
106
  readonly kind: "node";
139
107
  readonly [runnableNodeInputType]?: TInputJson;
140
108
  readonly [runnableNodeOutputType]?: TOutputJson;
141
- readonly [runnableNodeWireType]?: TWireJson;
142
109
  /**
143
- * Optional Zod input contract for {@link ItemNode} when not set on the node class.
110
+ * Optional Zod input contract for {@link RunnableNode} when not set on the node class.
144
111
  * Resolution order: node instance `inputSchema`, then config `inputSchema`, then `z.unknown()`.
145
112
  */
146
113
  readonly inputSchema?: ZodType<TInputJson>;
147
114
  /**
148
- * Optional per-item mapper: engine applies it before validating against the node’s `inputSchema`.
149
- * When omitted, the engine validates `item.json` directly.
115
+ * Overrides default lineage propagation for `execute` outputs (binary/meta/paired).
116
+ * Routers with multiple {@link RunnableNode#outputPorts} default to **`carryThrough`**; others default to **`emitOnly`**.
117
+ */
118
+ readonly lineageCarry?: LineageCarryPolicy;
119
+ /**
120
+ * When an activation receives **zero** input items, the engine normally runs `execute` zero times.
121
+ * Set to **`runOnce`** to run `execute` once with an empty `items` batch (and a synthetic wire item for schema parsing).
122
+ * Used by batch-style callback nodes (built-in `Callback`) so `callback([], ctx)` still runs.
150
123
  */
151
- readonly mapInput?: ItemInputMapper<TWireJson, TInputJson>;
124
+ readonly emptyBatchExecution?: "skip" | "runOnce";
152
125
  }
153
126
 
154
127
  export declare const triggerNodeSetupStateType: unique symbol;
@@ -162,14 +135,11 @@ export interface TriggerNodeConfig<
162
135
  readonly [triggerNodeSetupStateType]?: TSetupState;
163
136
  }
164
137
 
165
- export type RunnableNodeInputJson<TConfig extends RunnableNodeConfig<any, any, any>> =
166
- TConfig extends RunnableNodeConfig<infer TInputJson, any, any> ? TInputJson : never;
167
-
168
- export type RunnableNodeWireJson<TConfig extends RunnableNodeConfig<any, any, any>> =
169
- TConfig extends RunnableNodeConfig<any, any, infer TWireJson> ? TWireJson : never;
138
+ export type RunnableNodeInputJson<TConfig extends RunnableNodeConfig<any, any>> =
139
+ TConfig extends RunnableNodeConfig<infer TInputJson, any> ? TInputJson : never;
170
140
 
171
- export type RunnableNodeOutputJson<TConfig extends RunnableNodeConfig<any, any, any>> =
172
- TConfig extends RunnableNodeConfig<any, infer TOutputJson, any> ? TOutputJson : never;
141
+ export type RunnableNodeOutputJson<TConfig extends RunnableNodeConfig<any, any>> =
142
+ TConfig extends RunnableNodeConfig<any, infer TOutputJson> ? TOutputJson : never;
173
143
 
174
144
  export type TriggerNodeOutputJson<TConfig extends TriggerNodeConfig<any, any>> =
175
145
  TConfig extends TriggerNodeConfig<infer TOutputJson, any> ? TOutputJson : never;
@@ -0,0 +1,67 @@
1
+ import type { InputPortKey, Item, Items, NodeInputsByPort } from "../types";
2
+
3
+ import { getOriginIndexFromItem } from "../contracts/itemMeta";
4
+
5
+ /**
6
+ * Default fan-in: combine multi-port {@link NodeInputsByPort} into one {@link Items} batch for per-item nodes.
7
+ *
8
+ * This is used when a single-input per-item node has multiple inbound edges (for example, branch reconverge
9
+ * after an `If` / `Switch`). The default behavior is **append / union** (preserving item payloads) with a
10
+ * deterministic order:
11
+ *
12
+ * - When router origin metadata exists (`meta._cm.originIndex`), items are sorted by origin index so the
13
+ * downstream batch preserves original ordering across branches.
14
+ * - Otherwise, items are appended by port-key order, preserving each port's local order.
15
+ */
16
+ export class FanInMergeByOriginMerger {
17
+ merge(inputsByPort: NodeInputsByPort): Items {
18
+ const portKeys = Object.keys(inputsByPort).sort();
19
+ if (portKeys.length === 0) {
20
+ return [];
21
+ }
22
+ if (portKeys.length === 1) {
23
+ const only = portKeys[0]!;
24
+ return [...(inputsByPort[only] ?? [])];
25
+ }
26
+
27
+ type Entry = Readonly<{
28
+ portKey: InputPortKey;
29
+ portIndex: number;
30
+ item: Item;
31
+ originIndex: number | undefined;
32
+ }>;
33
+
34
+ const entries: Entry[] = [];
35
+ let anyOrigin = false;
36
+
37
+ for (let p = 0; p < portKeys.length; p++) {
38
+ const portKey = portKeys[p]!;
39
+ const items = inputsByPort[portKey] ?? [];
40
+ for (let i = 0; i < items.length; i++) {
41
+ const item = items[i] as Item;
42
+ const originIndex = getOriginIndexFromItem(item);
43
+ if (originIndex !== undefined) {
44
+ anyOrigin = true;
45
+ }
46
+ entries.push({ portKey, portIndex: i, item, originIndex });
47
+ }
48
+ }
49
+
50
+ if (!anyOrigin) {
51
+ return entries.map((e) => e.item);
52
+ }
53
+
54
+ const missingOriginRank = Number.MAX_SAFE_INTEGER;
55
+ return entries
56
+ .slice()
57
+ .sort((a, b) => {
58
+ const ao = a.originIndex ?? missingOriginRank;
59
+ const bo = b.originIndex ?? missingOriginRank;
60
+ if (ao !== bo) return ao - bo;
61
+ const pk = a.portKey.localeCompare(b.portKey);
62
+ if (pk !== 0) return pk;
63
+ return a.portIndex - b.portIndex;
64
+ })
65
+ .map((e) => e.item);
66
+ }
67
+ }
@@ -0,0 +1,27 @@
1
+ import { resolveItemValuesForExecution } from "../contracts/itemValue";
2
+ import type { Item, NodeExecutionContext, RunnableNodeConfig } from "../types";
3
+
4
+ /**
5
+ * Resolves {@link import("../contracts/itemValue").ItemValue} leaves on runnable config before {@link RunnableNode.execute}.
6
+ */
7
+ export class ItemValueResolver {
8
+ async resolveConfigForItem<TConfig extends RunnableNodeConfig<any, any>>(
9
+ ctx: NodeExecutionContext<TConfig>,
10
+ item: Item,
11
+ itemIndex: number,
12
+ items: ReadonlyArray<Item>,
13
+ ): Promise<NodeExecutionContext<TConfig>> {
14
+ if (!ctx) {
15
+ throw new Error("ItemValueResolver.resolveConfigForItem: ctx is required");
16
+ }
17
+ const resolvedConfig = await resolveItemValuesForExecution(ctx.config, ctx, item, itemIndex, items);
18
+ const merged = resolvedConfig !== undefined && resolvedConfig !== null ? resolvedConfig : ctx.config;
19
+ if (merged === undefined || merged === null) {
20
+ return ctx;
21
+ }
22
+ return {
23
+ ...ctx,
24
+ config: merged as TConfig,
25
+ };
26
+ }
27
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  NodeActivationRequest,
7
7
  NodeExecutionContext,
8
8
  NodeId,
9
+ NodeInputsByPort,
9
10
  ParentExecutionRef,
10
11
  RunDataFactory,
11
12
  RunExecutionOptions,
@@ -34,6 +35,12 @@ export type SingleDefinitionActivationRequest = NodeActivationContextArgs & {
34
35
  input: Items;
35
36
  };
36
37
 
38
+ export type MultiDefinitionActivationRequest = NodeActivationContextArgs & {
39
+ definition: NodeExecutionDefinition;
40
+ batchId: string;
41
+ inputsByPort: NodeInputsByPort;
42
+ };
43
+
37
44
  export type PlannedNodeActivationRequest = NodeActivationContextArgs & {
38
45
  next: PlannedActivation;
39
46
  nodeDefinition: NodeExecutionDefinition;
@@ -75,6 +82,24 @@ export class NodeActivationRequestComposer {
75
82
  };
76
83
  }
77
84
 
85
+ createMultiFromDefinitionWithActivation(
86
+ args: MultiDefinitionActivationRequest & Readonly<{ activationId: NodeActivationId }>,
87
+ ): Extract<NodeActivationRequest, { kind: "multi" }> {
88
+ const ctx = this.createNodeExecutionContext(args, args.definition, args.activationId);
89
+ return {
90
+ kind: "multi",
91
+ runId: args.runId,
92
+ activationId: args.activationId,
93
+ workflowId: args.workflowId,
94
+ nodeId: args.definition.id,
95
+ parent: args.parent,
96
+ executionOptions: args.executionOptions,
97
+ batchId: args.batchId,
98
+ inputsByPort: args.inputsByPort,
99
+ ctx,
100
+ };
101
+ }
102
+
78
103
  createFromPlannedActivation(args: PlannedNodeActivationRequest): NodeActivationRequest {
79
104
  const activationId = this.activationIdFactory.makeActivationId();
80
105
  const ctx = this.createNodeExecutionContext(args, args.nodeDefinition, activationId);
@@ -2,40 +2,62 @@ import { z, ZodError } from "zod";
2
2
 
3
3
  import type { Item, NodeActivationRequest, RunnableNodeConfig, WorkflowNodeInstanceFactory } from "../types";
4
4
 
5
+ import { FanInMergeByOriginMerger } from "./FanInMergeByOriginMerger";
5
6
  import { NodeInputContractError } from "./NodeInputContractError";
6
7
 
7
8
  /**
8
- * Maps and validates per-item inputs for {@link ItemNode} before enqueue persistence.
9
+ * Validates per-item inputs for {@link RunnableNode} before enqueue persistence (Zod on `item.json`).
10
+ * Does not rewrite `item.json` (wire stays as emitted upstream; engine passes parsed input via `execute` args).
11
+ * Converts multi-input activations into a single-input batch when the node is per-item only (engine fan-in).
9
12
  */
10
13
  export class NodeActivationRequestInputPreparer {
14
+ private readonly fanInMerger = new FanInMergeByOriginMerger();
15
+
11
16
  constructor(private readonly workflowNodeInstanceFactory: WorkflowNodeInstanceFactory) {}
12
17
 
13
18
  async prepare(request: NodeActivationRequest): Promise<NodeActivationRequest> {
14
- if (request.kind !== "single") {
19
+ if (request.kind === "multi") {
20
+ return await this.prepareMulti(request);
21
+ }
22
+ return await this.prepareSingle(request);
23
+ }
24
+
25
+ private async prepareMulti(
26
+ request: Extract<NodeActivationRequest, { kind: "multi" }>,
27
+ ): Promise<NodeActivationRequest> {
28
+ const nodeInstance: unknown = this.workflowNodeInstanceFactory.createByType(request.ctx.config.type);
29
+ if (
30
+ !this.hasRunnableExecute(nodeInstance) ||
31
+ this.hasExecuteMulti(nodeInstance) ||
32
+ this.isTriggerNode(nodeInstance)
33
+ ) {
15
34
  return request;
16
35
  }
36
+ const merged = this.fanInMerger.merge(request.inputsByPort);
37
+ const single: Extract<NodeActivationRequest, { kind: "single" }> = {
38
+ ...request,
39
+ kind: "single",
40
+ input: merged,
41
+ };
42
+ return await this.prepareSingle(single);
43
+ }
44
+
45
+ private async prepareSingle(
46
+ request: Extract<NodeActivationRequest, { kind: "single" }>,
47
+ ): Promise<NodeActivationRequest> {
17
48
  const nodeInstance: unknown = this.workflowNodeInstanceFactory.createByType(request.ctx.config.type);
18
- if (!this.hasExecuteOne(nodeInstance)) {
49
+ if (!this.hasRunnableExecute(nodeInstance) || this.isTriggerNode(nodeInstance)) {
19
50
  return request;
20
51
  }
21
52
  const inputSchema = this.resolveInputSchema(nodeInstance, request.ctx.config as RunnableNodeConfig);
22
- const config = request.ctx.config as RunnableNodeConfig;
23
- const mappedItems: Item[] = [];
24
- for (let i = 0; i < request.input.length; i++) {
25
- const item = request.input[i] as Item;
53
+ const inputBatch = request.input ?? [];
54
+ for (let i = 0; i < inputBatch.length; i++) {
55
+ const item = inputBatch[i] as Item;
26
56
  try {
27
- const mappedRaw = config.mapInput
28
- ? await Promise.resolve(
29
- config.mapInput({
30
- item,
31
- itemIndex: i,
32
- items: request.input,
33
- ctx: request.ctx,
34
- }),
35
- )
36
- : item.json;
37
- const parsed = inputSchema.parse(mappedRaw);
38
- mappedItems.push({ ...item, json: parsed });
57
+ if (Array.isArray(item.json)) {
58
+ throw new Error("Item JSON must not be a top-level array");
59
+ }
60
+ inputSchema.parse(item.json);
39
61
  } catch (cause) {
40
62
  const message = this.formatContractFailure(cause);
41
63
  throw new NodeInputContractError(
@@ -46,20 +68,30 @@ export class NodeActivationRequestInputPreparer {
46
68
  );
47
69
  }
48
70
  }
49
- return {
50
- ...request,
51
- input: mappedItems,
52
- };
71
+ return request.input === undefined ? { ...request, input: inputBatch } : request;
53
72
  }
54
73
 
55
- private hasExecuteOne(nodeInstance: unknown): boolean {
74
+ private isTriggerNode(nodeInstance: unknown): boolean {
56
75
  return (
57
76
  typeof nodeInstance === "object" &&
58
77
  nodeInstance !== null &&
59
- typeof (nodeInstance as { executeOne?: unknown }).executeOne === "function"
78
+ (nodeInstance as { kind?: string }).kind === "trigger"
60
79
  );
61
80
  }
62
81
 
82
+ private hasRunnableExecute(nodeInstance: unknown): boolean {
83
+ return (
84
+ typeof nodeInstance === "object" &&
85
+ nodeInstance !== null &&
86
+ (nodeInstance as { kind?: string }).kind === "node" &&
87
+ typeof (nodeInstance as { execute?: unknown }).execute === "function"
88
+ );
89
+ }
90
+
91
+ private hasExecuteMulti(nodeInstance: unknown): boolean {
92
+ return typeof (nodeInstance as { executeMulti?: unknown }).executeMulti === "function";
93
+ }
94
+
63
95
  private resolveInputSchema(
64
96
  nodeInstance: unknown,
65
97
  config: RunnableNodeConfig,