@codemation/core 0.2.3 → 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 +23 -0
- package/README.md +2 -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-BDHCiN22.d.ts → index-CUt13qs1.d.ts} +85 -16
- package/dist/index.cjs +74 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -24
- package/dist/index.d.ts +3 -3
- package/dist/index.js +64 -13
- 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 +144 -25
- package/src/authoring/index.ts +3 -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
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
149
|
-
*
|
|
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
|
|
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
|
|
166
|
-
TConfig extends RunnableNodeConfig<infer TInputJson, any
|
|
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
|
|
172
|
-
TConfig extends RunnableNodeConfig<any, infer TOutputJson
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
74
|
+
private isTriggerNode(nodeInstance: unknown): boolean {
|
|
56
75
|
return (
|
|
57
76
|
typeof nodeInstance === "object" &&
|
|
58
77
|
nodeInstance !== null &&
|
|
59
|
-
|
|
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,
|
|
@@ -1,80 +1,249 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isPortsEmission, isUnbrandedPortsEmissionShape } from "../contracts/emitPorts";
|
|
3
|
+
|
|
1
4
|
import type {
|
|
2
5
|
Item,
|
|
6
|
+
LineageCarryPolicy,
|
|
3
7
|
MultiInputNode,
|
|
4
|
-
Node,
|
|
5
8
|
NodeActivationRequest,
|
|
9
|
+
NodeConfigBase,
|
|
10
|
+
NodeExecutionContext,
|
|
6
11
|
NodeOutputs,
|
|
12
|
+
RunnableNode,
|
|
13
|
+
RunnableNodeConfig,
|
|
14
|
+
RunnableNodeExecuteArgs,
|
|
15
|
+
TriggerNode,
|
|
7
16
|
WorkflowNodeInstanceFactory,
|
|
8
17
|
} from "../types";
|
|
9
18
|
|
|
19
|
+
import { FanInMergeByOriginMerger } from "./FanInMergeByOriginMerger";
|
|
20
|
+
import { ItemValueResolver } from "./ItemValueResolver";
|
|
10
21
|
import { InProcessRetryRunner } from "./InProcessRetryRunner";
|
|
22
|
+
import { NodeOutputNormalizer } from "./NodeOutputNormalizer";
|
|
11
23
|
|
|
12
24
|
export class NodeExecutor {
|
|
25
|
+
private readonly fanInMerger = new FanInMergeByOriginMerger();
|
|
26
|
+
private readonly outputNormalizer = new NodeOutputNormalizer();
|
|
27
|
+
private readonly itemValueResolver: ItemValueResolver;
|
|
28
|
+
|
|
13
29
|
constructor(
|
|
14
30
|
private readonly nodeInstanceFactory: WorkflowNodeInstanceFactory,
|
|
15
31
|
private readonly retryRunner: InProcessRetryRunner,
|
|
16
|
-
|
|
32
|
+
itemValueResolver?: ItemValueResolver,
|
|
33
|
+
) {
|
|
34
|
+
this.itemValueResolver = itemValueResolver ?? new ItemValueResolver();
|
|
35
|
+
}
|
|
17
36
|
|
|
18
37
|
async execute(request: NodeActivationRequest): Promise<NodeOutputs> {
|
|
19
38
|
const policy = request.ctx.config.retryPolicy;
|
|
20
39
|
return await this.retryRunner.run(policy, async () => {
|
|
21
40
|
const nodeInstance = this.nodeInstanceFactory.createByType(request.ctx.config.type);
|
|
22
41
|
if (request.kind === "multi") {
|
|
23
|
-
return await this.
|
|
42
|
+
return await this.executeMultiInputActivation(request, nodeInstance);
|
|
24
43
|
}
|
|
25
44
|
return await this.executeSingleInputNode(request, nodeInstance);
|
|
26
45
|
});
|
|
27
46
|
}
|
|
28
47
|
|
|
29
|
-
private async
|
|
48
|
+
private async executeMultiInputActivation(
|
|
30
49
|
request: Extract<NodeActivationRequest, { kind: "multi" }>,
|
|
31
50
|
node: unknown,
|
|
32
51
|
): Promise<NodeOutputs> {
|
|
33
52
|
const multiInputNode = node as MultiInputNode;
|
|
34
|
-
if (typeof (multiInputNode as { executeMulti?: unknown }).executeMulti
|
|
35
|
-
|
|
53
|
+
if (typeof (multiInputNode as { executeMulti?: unknown }).executeMulti === "function") {
|
|
54
|
+
const raw = await multiInputNode.executeMulti(request.inputsByPort, request.ctx as never);
|
|
55
|
+
this.assertNoPortEnvelopeBypass(request.nodeId, raw, "executeMulti()");
|
|
56
|
+
return raw;
|
|
36
57
|
}
|
|
37
|
-
|
|
58
|
+
if (this.isRunnableNode(node)) {
|
|
59
|
+
const merged = this.fanInMerger.merge(request.inputsByPort);
|
|
60
|
+
const single: Extract<NodeActivationRequest, { kind: "single" }> = {
|
|
61
|
+
...request,
|
|
62
|
+
kind: "single",
|
|
63
|
+
input: merged,
|
|
64
|
+
};
|
|
65
|
+
return await this.executeRunnableActivation(single, node);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Node ${request.nodeId} does not support executeMulti or RunnableNode.execute but received multi-input activation`,
|
|
69
|
+
);
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
private async executeSingleInputNode(
|
|
41
73
|
request: Extract<NodeActivationRequest, { kind: "single" }>,
|
|
42
74
|
node: unknown,
|
|
43
75
|
): Promise<NodeOutputs> {
|
|
44
|
-
if (this.
|
|
45
|
-
|
|
76
|
+
if (this.isTriggerNode(node)) {
|
|
77
|
+
const raw = await (node as TriggerNode).execute(request.input, request.ctx as never);
|
|
78
|
+
this.assertNoPortEnvelopeBypass(request.nodeId, raw, "trigger execute()");
|
|
79
|
+
return raw;
|
|
46
80
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
throw new Error(`Node ${request.nodeId} does not support execute but received single-input activation`);
|
|
81
|
+
if (this.isRunnableNode(node)) {
|
|
82
|
+
return await this.executeRunnableActivation(request, node);
|
|
50
83
|
}
|
|
51
|
-
|
|
84
|
+
if (this.hasExecuteMulti(node)) {
|
|
85
|
+
return await this.executeMultiInputActivation(this.asMultiFromSingleActivation(request), node);
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Node ${request.nodeId} does not support trigger or RunnableNode execution`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private isTriggerNode(node: unknown): node is TriggerNode {
|
|
91
|
+
return typeof node === "object" && node !== null && (node as { kind?: string }).kind === "trigger";
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
private
|
|
94
|
+
private isRunnableNode(node: unknown): node is RunnableNode {
|
|
55
95
|
return (
|
|
56
|
-
typeof node === "object" &&
|
|
96
|
+
typeof node === "object" &&
|
|
97
|
+
node !== null &&
|
|
98
|
+
(node as { kind?: string }).kind === "node" &&
|
|
99
|
+
typeof (node as { execute?: unknown }).execute === "function"
|
|
57
100
|
);
|
|
58
101
|
}
|
|
59
102
|
|
|
60
|
-
private
|
|
103
|
+
private hasExecuteMulti(node: unknown): boolean {
|
|
104
|
+
return typeof (node as { executeMulti?: unknown })?.executeMulti === "function";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private asMultiFromSingleActivation(
|
|
61
108
|
request: Extract<NodeActivationRequest, { kind: "single" }>,
|
|
62
|
-
|
|
109
|
+
): Extract<NodeActivationRequest, { kind: "multi" }> {
|
|
110
|
+
return {
|
|
111
|
+
kind: "multi",
|
|
112
|
+
runId: request.runId,
|
|
113
|
+
activationId: request.activationId,
|
|
114
|
+
workflowId: request.workflowId,
|
|
115
|
+
nodeId: request.nodeId,
|
|
116
|
+
parent: request.parent,
|
|
117
|
+
executionOptions: request.executionOptions,
|
|
118
|
+
batchId: request.batchId,
|
|
119
|
+
ctx: request.ctx,
|
|
120
|
+
inputsByPort: { in: request.input ?? [] },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async executeRunnableActivation(
|
|
125
|
+
request: Extract<NodeActivationRequest, { kind: "single" }>,
|
|
126
|
+
node: RunnableNode,
|
|
63
127
|
): Promise<NodeOutputs> {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
128
|
+
const runnableConfig = request.ctx.config as RunnableNodeConfig;
|
|
129
|
+
const carry = this.resolveLineageCarry(node, runnableConfig);
|
|
130
|
+
const inputSchema = this.resolveInputSchema(node, runnableConfig);
|
|
131
|
+
const inputBatch = request.input ?? [];
|
|
132
|
+
if (inputBatch.length === 0 && runnableConfig.emptyBatchExecution === "runOnce") {
|
|
133
|
+
const syntheticItem: Item = { json: {} };
|
|
134
|
+
const parsed = inputSchema.parse(syntheticItem.json);
|
|
135
|
+
const runnableCtx = request.ctx as NodeExecutionContext<RunnableNodeConfig>;
|
|
136
|
+
const resolvedCtx = await this.itemValueResolver.resolveConfigForItem(runnableCtx, syntheticItem, 0, inputBatch);
|
|
137
|
+
const ctx = this.pickExecutionContext(runnableCtx, resolvedCtx);
|
|
138
|
+
const args: RunnableNodeExecuteArgs = {
|
|
139
|
+
input: parsed,
|
|
140
|
+
item: syntheticItem,
|
|
141
|
+
itemIndex: 0,
|
|
142
|
+
items: inputBatch,
|
|
143
|
+
ctx,
|
|
144
|
+
};
|
|
145
|
+
const raw = await Promise.resolve(node.execute(args));
|
|
146
|
+
return this.outputNormalizer.normalizeExecuteResult({
|
|
147
|
+
baseItem: syntheticItem,
|
|
148
|
+
raw,
|
|
149
|
+
carry,
|
|
150
|
+
}) as NodeOutputs;
|
|
151
|
+
}
|
|
152
|
+
const byPort: Partial<Record<string, Item[]>> = {};
|
|
153
|
+
for (let i = 0; i < inputBatch.length; i++) {
|
|
154
|
+
const item = inputBatch[i] as Item;
|
|
155
|
+
this.assertItemJsonNotTopLevelArray(request.nodeId, item);
|
|
156
|
+
const parsed = inputSchema.parse(item.json);
|
|
157
|
+
const runnableCtx = request.ctx as NodeExecutionContext<RunnableNodeConfig>;
|
|
158
|
+
const resolvedCtx = await this.itemValueResolver.resolveConfigForItem(runnableCtx, item, i, inputBatch);
|
|
159
|
+
const ctx = this.pickExecutionContext(runnableCtx, resolvedCtx);
|
|
160
|
+
const args: RunnableNodeExecuteArgs = {
|
|
161
|
+
input: parsed,
|
|
162
|
+
item,
|
|
163
|
+
itemIndex: i,
|
|
164
|
+
items: inputBatch,
|
|
165
|
+
ctx,
|
|
166
|
+
};
|
|
167
|
+
const raw = await Promise.resolve(node.execute(args));
|
|
168
|
+
const normalized = this.outputNormalizer.normalizeExecuteResult({
|
|
169
|
+
baseItem: item,
|
|
170
|
+
raw,
|
|
171
|
+
carry,
|
|
172
|
+
});
|
|
173
|
+
for (const [port, batch] of Object.entries(normalized)) {
|
|
174
|
+
if (!batch || batch.length === 0) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const list = byPort[port] ?? [];
|
|
178
|
+
list.push(...batch);
|
|
179
|
+
byPort[port] = list;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return byPort as NodeOutputs;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Use resolver ctx only when {@link NodeExecutionContext.config} is non-nullish. */
|
|
186
|
+
private pickExecutionContext<TConfig extends RunnableNodeConfig<any, any>>(
|
|
187
|
+
runnableCtx: NodeExecutionContext<TConfig>,
|
|
188
|
+
resolvedCtx: NodeExecutionContext<TConfig> | null | undefined,
|
|
189
|
+
): NodeExecutionContext<TConfig> {
|
|
190
|
+
if (resolvedCtx != null && resolvedCtx.config != null) {
|
|
191
|
+
return resolvedCtx;
|
|
192
|
+
}
|
|
193
|
+
return runnableCtx;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private resolveInputSchema(
|
|
197
|
+
nodeInstance: RunnableNode,
|
|
198
|
+
config: RunnableNodeConfig,
|
|
199
|
+
): {
|
|
200
|
+
parse: (data: unknown) => unknown;
|
|
201
|
+
} {
|
|
202
|
+
const fromInstance = nodeInstance.inputSchema;
|
|
203
|
+
if (fromInstance && typeof fromInstance.parse === "function") {
|
|
204
|
+
return fromInstance as { parse: (data: unknown) => unknown };
|
|
205
|
+
}
|
|
206
|
+
const fromConfig = config.inputSchema;
|
|
207
|
+
if (fromConfig && typeof fromConfig.parse === "function") {
|
|
208
|
+
return fromConfig as { parse: (data: unknown) => unknown };
|
|
209
|
+
}
|
|
210
|
+
return z.unknown();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private assertItemJsonNotTopLevelArray(nodeId: string, item: Item): void {
|
|
214
|
+
if (Array.isArray(item.json)) {
|
|
215
|
+
throw new Error(`Node ${nodeId}: item.json must not be a top-level JSON array`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private assertNoPortEnvelopeBypass(nodeId: string, value: unknown, methodName: string): void {
|
|
220
|
+
if (isPortsEmission(value)) {
|
|
221
|
+
throw new Error(`Node ${nodeId}: ${methodName} must return NodeOutputs, not emitPorts(...).`);
|
|
222
|
+
}
|
|
223
|
+
if (isUnbrandedPortsEmissionShape(value)) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Node ${nodeId}: ${methodName} returned an unbranded \`{ ports: ... }\` object. Return NodeOutputs instead.`,
|
|
75
226
|
);
|
|
76
|
-
out.push({ ...item, json: outputJson });
|
|
77
227
|
}
|
|
78
|
-
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private resolveLineageCarry(node: unknown, config: RunnableNodeConfig): LineageCarryPolicy {
|
|
231
|
+
if (config.lineageCarry) {
|
|
232
|
+
return config.lineageCarry;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const base = config as NodeConfigBase;
|
|
236
|
+
const declared = base.declaredOutputPorts;
|
|
237
|
+
const ports =
|
|
238
|
+
declared && declared.length > 0
|
|
239
|
+
? [...new Set([...(declared as readonly string[]), ...(base.nodeErrorHandler ? ["error"] : [])])]
|
|
240
|
+
: base.nodeErrorHandler
|
|
241
|
+
? (["main", "error"] as const)
|
|
242
|
+
: (["main"] as const);
|
|
243
|
+
|
|
244
|
+
if (ports.length > 1) {
|
|
245
|
+
return "carryThrough";
|
|
246
|
+
}
|
|
247
|
+
return "emitOnly";
|
|
79
248
|
}
|
|
80
249
|
}
|