@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +2 -0
  3. package/dist/{EngineRuntimeRegistration.types-Bjeo7Sfq.d.ts → EngineRuntimeRegistration.types-DU6MsjU9.d.ts} +2 -2
  4. package/dist/{EngineWorkflowRunnerService-Dd4yD31l.d.cts → EngineWorkflowRunnerService-BBkL4VQF.d.cts} +2 -2
  5. package/dist/{InMemoryRunDataFactory-OUzDmAHt.d.cts → InMemoryRunDataFactory-CsYEMJK2.d.cts} +11 -3
  6. package/dist/{RunIntentService-Bkg4oYrM.d.cts → RunIntentService-BvlTpmEb.d.cts} +224 -237
  7. package/dist/{RunIntentService-BAKikN8h.d.ts → RunIntentService-zbTchO9T.d.ts} +305 -259
  8. package/dist/bootstrap/index.cjs +2 -2
  9. package/dist/bootstrap/index.d.cts +19 -7
  10. package/dist/bootstrap/index.d.ts +3 -3
  11. package/dist/bootstrap/index.js +2 -2
  12. package/dist/{bootstrap-DwS5S7s9.cjs → bootstrap-DHH2uo-W.cjs} +4 -2
  13. package/dist/bootstrap-DHH2uo-W.cjs.map +1 -0
  14. package/dist/{bootstrap-BD6CobHl.js → bootstrap-DbUlOl11.js} +4 -2
  15. package/dist/bootstrap-DbUlOl11.js.map +1 -0
  16. package/dist/{index-BDHCiN22.d.ts → index-CUt13qs1.d.ts} +85 -16
  17. package/dist/index.cjs +74 -12
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +131 -24
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +64 -13
  22. package/dist/index.js.map +1 -1
  23. package/dist/{runtime-Cy-3FTI_.js → runtime-BdH94eBR.js} +502 -123
  24. package/dist/runtime-BdH94eBR.js.map +1 -0
  25. package/dist/{runtime-ZJUpWmPH.cjs → runtime-feFn8OmG.cjs} +561 -122
  26. package/dist/runtime-feFn8OmG.cjs.map +1 -0
  27. package/dist/testing.cjs +40 -36
  28. package/dist/testing.cjs.map +1 -1
  29. package/dist/testing.d.cts +17 -26
  30. package/dist/testing.d.ts +17 -26
  31. package/dist/testing.js +40 -36
  32. package/dist/testing.js.map +1 -1
  33. package/dist/{workflowActivationPolicy-BzyzXLa_.cjs → workflowActivationPolicy-6V3OJD3N.cjs} +65 -19
  34. package/dist/workflowActivationPolicy-6V3OJD3N.cjs.map +1 -0
  35. package/dist/{workflowActivationPolicy-B8HzTk3o.js → workflowActivationPolicy-Td9HTOuD.js} +65 -19
  36. package/dist/workflowActivationPolicy-Td9HTOuD.js.map +1 -0
  37. package/package.json +2 -1
  38. package/src/ai/AgentConfigInspectorFactory.ts +4 -0
  39. package/src/ai/AgentMessageConfigNormalizerFactory.ts +7 -0
  40. package/src/ai/AgentToolFactory.ts +2 -2
  41. package/src/ai/AiHost.ts +11 -10
  42. package/src/ai/NodeBackedToolConfig.ts +1 -1
  43. package/src/authoring/defineNode.types.ts +144 -25
  44. package/src/authoring/index.ts +3 -1
  45. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +8 -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
package/src/ai/AiHost.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { TypeToken } from "../di";
2
2
 
3
3
  import type { CredentialRequirement } from "../contracts/credentialTypes";
4
+ import type { ItemValue } from "../contracts/itemValue";
4
5
 
5
6
  import type {
6
7
  Item,
@@ -91,6 +92,7 @@ export type AgentMessageLine<TInputJson = unknown> = AgentMessageDto | AgentMess
91
92
  * Use the object form only when you need `buildMessages` to append messages after optional `prompt` lines.
92
93
  */
93
94
  export type AgentMessageConfig<TInputJson = unknown> =
95
+ | ItemValue<ReadonlyArray<AgentMessageLine<TInputJson>>, TInputJson>
94
96
  | ReadonlyArray<AgentMessageLine<TInputJson>>
95
97
  | {
96
98
  readonly prompt?: ReadonlyArray<AgentMessageLine<TInputJson>>;
@@ -150,7 +152,7 @@ export interface ChatModelFactory<TConfig extends ChatModelConfig = ChatModelCon
150
152
  }
151
153
 
152
154
  export type NodeBackedToolInputMapperArgs<
153
- TNodeConfig extends RunnableNodeConfig<any, any, any>,
155
+ TNodeConfig extends RunnableNodeConfig<any, any>,
154
156
  TToolInput = unknown,
155
157
  > = Readonly<{
156
158
  input: TToolInput;
@@ -162,7 +164,7 @@ export type NodeBackedToolInputMapperArgs<
162
164
  }>;
163
165
 
164
166
  export type NodeBackedToolOutputMapperArgs<
165
- TNodeConfig extends RunnableNodeConfig<any, any, any>,
167
+ TNodeConfig extends RunnableNodeConfig<any, any>,
166
168
  TToolInput = unknown,
167
169
  > = Readonly<{
168
170
  input: TToolInput;
@@ -174,18 +176,18 @@ export type NodeBackedToolOutputMapperArgs<
174
176
  outputs: NodeOutputs;
175
177
  }>;
176
178
 
177
- export type NodeBackedToolInputMapper<TNodeConfig extends RunnableNodeConfig<any, any, any>, TToolInput = unknown> = (
179
+ export type NodeBackedToolInputMapper<TNodeConfig extends RunnableNodeConfig<any, any>, TToolInput = unknown> = (
178
180
  args: NodeBackedToolInputMapperArgs<TNodeConfig, TToolInput>,
179
181
  ) => Item<RunnableNodeInputJson<TNodeConfig>> | RunnableNodeInputJson<TNodeConfig>;
180
182
 
181
183
  export type NodeBackedToolOutputMapper<
182
- TNodeConfig extends RunnableNodeConfig<any, any, any>,
184
+ TNodeConfig extends RunnableNodeConfig<any, any>,
183
185
  TToolInput = unknown,
184
186
  TToolOutput = unknown,
185
187
  > = (args: NodeBackedToolOutputMapperArgs<TNodeConfig, TToolInput>) => TToolOutput;
186
188
 
187
189
  export type NodeBackedToolConfigOptions<
188
- TNodeConfig extends RunnableNodeConfig<any, any, any>,
190
+ TNodeConfig extends RunnableNodeConfig<any, any>,
189
191
  TInputSchema extends ZodSchemaAny,
190
192
  TOutputSchema extends ZodSchemaAny,
191
193
  > = Readonly<{
@@ -197,11 +199,10 @@ export type NodeBackedToolConfigOptions<
197
199
  mapOutput?: NodeBackedToolOutputMapper<TNodeConfig, ZodInput<TInputSchema>, ZodOutput<TOutputSchema>>;
198
200
  }>;
199
201
 
200
- export interface AgentNodeConfig<
201
- TInputJson = unknown,
202
- TOutputJson = unknown,
203
- TWireJson = TInputJson,
204
- > extends RunnableNodeConfig<TInputJson, TOutputJson, TWireJson> {
202
+ export interface AgentNodeConfig<TInputJson = unknown, TOutputJson = unknown> extends RunnableNodeConfig<
203
+ TInputJson,
204
+ TOutputJson
205
+ > {
205
206
  readonly messages: AgentMessageConfig<TInputJson>;
206
207
  readonly chatModel: ChatModelConfig;
207
208
  readonly tools?: ReadonlyArray<ToolConfig>;
@@ -14,7 +14,7 @@ import type {
14
14
  } from "./AiHost";
15
15
 
16
16
  export class NodeBackedToolConfig<
17
- TNodeConfig extends RunnableNodeConfig<any, any, any>,
17
+ TNodeConfig extends RunnableNodeConfig<any, any>,
18
18
  TInputSchema extends ZodSchemaAny,
19
19
  TOutputSchema extends ZodSchemaAny,
20
20
  > implements ToolConfig {
@@ -4,10 +4,11 @@ import type {
4
4
  CredentialRequirement,
5
5
  CredentialTypeId,
6
6
  } from "../contracts/credentialTypes";
7
- import type { Node, NodeExecutionContext } from "../contracts/runtimeTypes";
8
- import type { Items, NodeOutputs, RunnableNodeConfig } from "../contracts/workflowTypes";
7
+ import type { RunnableNode, RunnableNodeExecuteArgs, NodeExecutionContext } from "../contracts/runtimeTypes";
8
+ import type { Item, Items, RunnableNodeConfig } from "../contracts/workflowTypes";
9
9
  import type { TypeToken } from "../di";
10
10
  import { node as persistedNode } from "../runtime-types/runtimeTypeDecorators.types";
11
+ import type { ZodType } from "zod";
11
12
  import { z } from "zod";
12
13
  import { DefinedNodeRegistry } from "./DefinedNodeRegistry";
13
14
 
@@ -54,6 +55,18 @@ export interface DefinedNodeRunContext<
54
55
  readonly execution: NodeExecutionContext<RunnableNodeConfig<TConfig, unknown>>;
55
56
  }
56
57
 
58
+ /**
59
+ * Arguments for {@link defineNode} `execute` (engine `ctx` matches {@link RunnableNode.execute};
60
+ * the second callback parameter adds {@link DefinedNodeRunContext} for credential accessors).
61
+ */
62
+ export type DefineNodeExecuteArgs<TConfig extends CredentialJsonRecord, TInputJson> = Readonly<{
63
+ input: TInputJson;
64
+ item: Item;
65
+ itemIndex: number;
66
+ items: Items;
67
+ ctx: NodeExecutionContext<RunnableNodeConfig<TInputJson, unknown> & Readonly<{ config: TConfig }>>;
68
+ }>;
69
+
57
70
  export interface DefinedNode<
58
71
  TKey extends string,
59
72
  TConfig extends CredentialJsonRecord,
@@ -69,6 +82,9 @@ export interface DefinedNode<
69
82
  register(context: { registerNode<TValue>(token: TypeToken<TValue>, implementation?: TypeToken<TValue>): void }): void;
70
83
  }
71
84
 
85
+ /**
86
+ * Plugin / DSL-friendly node: per-item `execute` with optional {@link RunnableNodeConfig.inputSchema}.
87
+ */
72
88
  export interface DefineNodeOptions<
73
89
  TKey extends string,
74
90
  TConfig extends CredentialJsonRecord,
@@ -84,6 +100,34 @@ export interface DefineNodeOptions<
84
100
  * The Next host resolves Lucide (`lucide:…`), built-in SVGs (`builtin:…`), Simple Icons (`si:…`), and image URLs (`https:`, `data:`, `/…`).
85
101
  */
86
102
  readonly icon?: string;
103
+ /** Default values / form hints for **static** node configuration (credentials, retry, IDs), not per-item payload. */
104
+ readonly input?: Readonly<Record<keyof TConfig & string, unknown>>;
105
+ readonly configSchema?: z.ZodType<TConfig>;
106
+ readonly credentials?: TBindings;
107
+ /**
108
+ * Validates **`input`** (engine also accepts `inputSchema` on the node class).
109
+ */
110
+ readonly inputSchema?: ZodType<TInputJson>;
111
+ execute(
112
+ args: DefineNodeExecuteArgs<TConfig, TInputJson>,
113
+ context: DefinedNodeRunContext<TConfig, TBindings>,
114
+ ): MaybePromise<TOutputJson>;
115
+ }
116
+
117
+ /**
118
+ * Batch-oriented defined node: `run` receives all item JSON once (last item in activation); emits one output per input row.
119
+ */
120
+ export interface DefineBatchNodeOptions<
121
+ TKey extends string,
122
+ TConfig extends CredentialJsonRecord,
123
+ TInputJson,
124
+ TOutputJson,
125
+ TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
126
+ > {
127
+ readonly key: TKey;
128
+ readonly title: string;
129
+ readonly description?: string;
130
+ readonly icon?: string;
87
131
  readonly input?: Readonly<Record<keyof TConfig & string, unknown>>;
88
132
  readonly configSchema?: z.ZodType<TConfig>;
89
133
  readonly credentials?: TBindings;
@@ -145,7 +189,7 @@ const definedNodeCredentialRequirementFactory = {
145
189
  const definedNodeCredentialAccessorFactory = {
146
190
  create<TBindings extends DefinedNodeCredentialBindings | undefined>(
147
191
  bindings: TBindings,
148
- ctx: NodeExecutionContext<RunnableNodeConfig<any, any, any>>,
192
+ ctx: NodeExecutionContext<RunnableNodeConfig<any, any>>,
149
193
  ): DefinedNodeCredentialAccessors<TBindings> {
150
194
  if (!bindings) {
151
195
  return {} as DefinedNodeCredentialAccessors<TBindings>;
@@ -167,32 +211,107 @@ export function defineNode<
167
211
  const credentialRequirements = definedNodeCredentialRequirementFactory.create(options.credentials);
168
212
  type DefinedRunnableNodeConfigShape = RunnableNodeConfig<TInputJson, TOutputJson> & Readonly<{ config: TConfig }>;
169
213
 
170
- const DefinedNodeRuntime = class implements Node<DefinedRunnableNodeConfigShape> {
214
+ const DefinedNodeRuntime = class implements RunnableNode<DefinedRunnableNodeConfigShape, TInputJson, TOutputJson> {
171
215
  readonly kind = "node" as const;
172
216
  readonly outputPorts = ["main"] as const;
217
+ readonly inputSchema = options.inputSchema;
173
218
 
174
- async execute(items: Items, ctx: NodeExecutionContext<DefinedRunnableNodeConfigShape>): Promise<NodeOutputs> {
175
- const outputs = await options.run(
176
- items.map((item) => item.json as TInputJson),
177
- {
178
- config: ctx.config.config,
179
- credentials: definedNodeCredentialAccessorFactory.create(
180
- options.credentials,
181
- ctx,
182
- ) as DefinedNodeCredentialAccessors<TBindings>,
183
- execution: ctx as unknown as NodeExecutionContext<RunnableNodeConfig<TConfig, unknown>>,
184
- },
185
- );
219
+ async execute(
220
+ args: Readonly<RunnableNodeExecuteArgs<DefinedRunnableNodeConfigShape, TInputJson>>,
221
+ ): Promise<unknown> {
222
+ const ctx = args.ctx;
223
+ const context: DefinedNodeRunContext<TConfig, TBindings> = {
224
+ config: ctx.config.config,
225
+ credentials: definedNodeCredentialAccessorFactory.create(
226
+ options.credentials,
227
+ ctx,
228
+ ) as DefinedNodeCredentialAccessors<TBindings>,
229
+ execution: ctx as unknown as NodeExecutionContext<RunnableNodeConfig<TConfig, unknown>>,
230
+ };
231
+ const payload: DefineNodeExecuteArgs<TConfig, TInputJson> = {
232
+ input: args.input,
233
+ item: args.item,
234
+ itemIndex: args.itemIndex,
235
+ items: args.items,
236
+ ctx,
237
+ };
238
+ return await options.execute(payload, context);
239
+ }
240
+ };
186
241
 
187
- return {
188
- main: outputs.map((json, index) => {
189
- const existing = items[index];
190
- if (!existing) {
191
- return { json };
192
- }
193
- return { ...existing, json };
194
- }),
242
+ persistedNode({ name: options.key })(DefinedNodeRuntime);
243
+
244
+ const DefinedRunnableNodeConfig = class implements RunnableNodeConfig<TInputJson, TOutputJson> {
245
+ readonly kind = "node" as const;
246
+ readonly type: TypeToken<unknown> = DefinedNodeRuntime;
247
+ readonly icon = options.icon;
248
+ readonly inputSchema = options.inputSchema;
249
+
250
+ constructor(
251
+ public readonly name: string,
252
+ public readonly config: TConfig,
253
+ public readonly id?: string,
254
+ ) {}
255
+
256
+ getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
257
+ return credentialRequirements;
258
+ }
259
+ };
260
+
261
+ const definition: DefinedNode<TKey, TConfig, TInputJson, TOutputJson, TBindings> = {
262
+ kind: "defined-node",
263
+ key: options.key,
264
+ title: options.title,
265
+ description: options.description,
266
+ create(config, name = options.title, id) {
267
+ return new DefinedRunnableNodeConfig(name, config, id);
268
+ },
269
+ register(context) {
270
+ context.registerNode(DefinedNodeRuntime);
271
+ },
272
+ };
273
+
274
+ DefinedNodeRegistry.register(definition as DefinedNode<string, Record<string, unknown>, unknown, unknown, undefined>);
275
+
276
+ return definition;
277
+ }
278
+
279
+ export function defineBatchNode<
280
+ TKey extends string,
281
+ TConfig extends CredentialJsonRecord,
282
+ TInputJson,
283
+ TOutputJson,
284
+ TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
285
+ >(
286
+ options: DefineBatchNodeOptions<TKey, TConfig, TInputJson, TOutputJson, TBindings>,
287
+ ): DefinedNode<TKey, TConfig, TInputJson, TOutputJson, TBindings> {
288
+ const credentialRequirements = definedNodeCredentialRequirementFactory.create(options.credentials);
289
+ type DefinedRunnableNodeConfigShape = RunnableNodeConfig<TInputJson, TOutputJson> & Readonly<{ config: TConfig }>;
290
+
291
+ const DefinedNodeRuntime = class implements RunnableNode<DefinedRunnableNodeConfigShape, TInputJson, TOutputJson> {
292
+ readonly kind = "node" as const;
293
+ readonly outputPorts = ["main"] as const;
294
+
295
+ async execute(
296
+ args: Readonly<RunnableNodeExecuteArgs<DefinedRunnableNodeConfigShape, TInputJson>>,
297
+ ): Promise<unknown> {
298
+ if (args.itemIndex !== args.items.length - 1) {
299
+ return [];
300
+ }
301
+ const ctx = args.ctx;
302
+ const context: DefinedNodeRunContext<TConfig, TBindings> = {
303
+ config: ctx.config.config,
304
+ credentials: definedNodeCredentialAccessorFactory.create(
305
+ options.credentials,
306
+ ctx,
307
+ ) as DefinedNodeCredentialAccessors<TBindings>,
308
+ execution: ctx as unknown as NodeExecutionContext<RunnableNodeConfig<TConfig, unknown>>,
195
309
  };
310
+ const outputs = await options.run(
311
+ args.items.map((item) => item.json as TInputJson),
312
+ context,
313
+ );
314
+ return [...outputs];
196
315
  }
197
316
  };
198
317
 
@@ -227,7 +346,7 @@ export function defineNode<
227
346
  },
228
347
  };
229
348
 
230
- DefinedNodeRegistry.register(definition as DefinedNode<string, Record<string, unknown>, unknown, unknown>);
349
+ DefinedNodeRegistry.register(definition as DefinedNode<string, Record<string, unknown>, unknown, unknown, undefined>);
231
350
 
232
351
  return definition;
233
352
  }
@@ -5,8 +5,10 @@ export type {
5
5
  DefinedNodeCredentialBinding,
6
6
  DefinedNodeCredentialBindings,
7
7
  DefinedNodeRunContext,
8
+ DefineBatchNodeOptions,
9
+ DefineNodeExecuteArgs,
8
10
  DefineNodeOptions,
9
11
  } from "./defineNode.types";
10
- export { defineNode } from "./defineNode.types";
12
+ export { defineBatchNode, defineNode } from "./defineNode.types";
11
13
  export type { DefineCredentialOptions } from "./defineCredential.types";
12
14
  export { defineCredential } from "./defineCredential.types";
@@ -4,9 +4,11 @@ import { EngineExecutionLimitsPolicyFactory } from "../../policies/executionLimi
4
4
  import {
5
5
  DefaultAsyncSleeper,
6
6
  InProcessRetryRunnerFactory,
7
+ ItemValueResolver,
7
8
  NodeExecutor,
8
9
  NodeExecutorFactory,
9
10
  NodeInstanceFactoryFactory,
11
+ NodeOutputNormalizer,
10
12
  } from "../../execution";
11
13
  import {
12
14
  EngineFactory,
@@ -38,6 +40,12 @@ export class EngineRuntimeRegistrar {
38
40
  }
39
41
 
40
42
  private registerSupportFactories(container: DependencyContainer): void {
43
+ if (!container.isRegistered(ItemValueResolver, true)) {
44
+ container.registerSingleton(ItemValueResolver, ItemValueResolver);
45
+ }
46
+ if (!container.isRegistered(NodeOutputNormalizer, true)) {
47
+ container.registerSingleton(NodeOutputNormalizer, NodeOutputNormalizer);
48
+ }
41
49
  container.register(EngineExecutionLimitsPolicyFactory, { useClass: EngineExecutionLimitsPolicyFactory });
42
50
  container.register(NodeInstanceFactoryFactory, { useClass: NodeInstanceFactoryFactory });
43
51
  container.register(DefaultAsyncSleeper, { useClass: DefaultAsyncSleeper });
@@ -0,0 +1,27 @@
1
+ import type { Items, JsonNonArray, OutputPortKey } from "./workflowTypes";
2
+
3
+ const EMIT_PORTS_BRAND = Symbol.for("codemation.emitPorts");
4
+
5
+ export type PortsEmission = Readonly<{
6
+ readonly [EMIT_PORTS_BRAND]: true;
7
+ readonly ports: Readonly<Partial<Record<OutputPortKey, Items | ReadonlyArray<JsonNonArray>>>>;
8
+ }>;
9
+
10
+ export function emitPorts(
11
+ ports: Readonly<Partial<Record<OutputPortKey, Items | ReadonlyArray<JsonNonArray>>>>,
12
+ ): PortsEmission {
13
+ return { [EMIT_PORTS_BRAND]: true, ports };
14
+ }
15
+
16
+ export function isPortsEmission(value: unknown): value is PortsEmission {
17
+ return (
18
+ typeof value === "object" &&
19
+ value !== null &&
20
+ EMIT_PORTS_BRAND in value &&
21
+ (value as Record<symbol, unknown>)[EMIT_PORTS_BRAND] === true
22
+ );
23
+ }
24
+
25
+ export function isUnbrandedPortsEmissionShape(value: unknown): value is Readonly<{ ports: unknown }> {
26
+ return typeof value === "object" && value !== null && "ports" in value && !isPortsEmission(value);
27
+ }
@@ -1,5 +1,8 @@
1
1
  export * from "./credentialTypes";
2
+ export * from "./emitPorts";
2
3
  export * from "./executionPersistenceContracts";
4
+ export * from "./itemMeta";
5
+ export * from "./itemValue";
3
6
  export * from "./runtimeTypes";
4
7
  export * from "./runFinishedAtFactory";
5
8
  export * from "./runTypes";
@@ -0,0 +1,11 @@
1
+ import type { Item } from "./workflowTypes";
2
+
3
+ /**
4
+ * Reads `meta._cm.originIndex` when present (used for fan-in merge-by-origin and Merge routing).
5
+ */
6
+ export function getOriginIndexFromItem(item: Item): number | undefined {
7
+ const meta = item.meta as Record<string, unknown> | undefined;
8
+ const cm = meta?._cm as Record<string, unknown> | undefined;
9
+ const v = cm?.originIndex;
10
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
11
+ }
@@ -0,0 +1,147 @@
1
+ import type { NodeExecutionContext } from "./runtimeTypes";
2
+ import type { Item, Items, NodeActivationId, NodeId, RunDataSnapshot, RunId, WorkflowId } from "./workflowTypes";
3
+
4
+ const ITEM_VALUE_BRAND = Symbol.for("codemation.itemValue");
5
+
6
+ export type ItemValueResolvedContext = Readonly<{
7
+ runId: RunId;
8
+ workflowId: WorkflowId;
9
+ nodeId: NodeId;
10
+ activationId: NodeActivationId;
11
+ data: RunDataSnapshot;
12
+ }>;
13
+
14
+ /**
15
+ * Context aligned with former {@link ItemInputMapperContext} — use **`data`** to read any completed upstream node.
16
+ */
17
+ export type ItemValueContext = ItemValueResolvedContext;
18
+
19
+ export type ItemValueArgs<TItemJson = unknown> = Readonly<{
20
+ item: Item<TItemJson>;
21
+ itemIndex: number;
22
+ items: Items<TItemJson>;
23
+ ctx: ItemValueContext;
24
+ }>;
25
+
26
+ export type ItemValueCallback<T, TItemJson = unknown> = (args: ItemValueArgs<TItemJson>) => T | Promise<T>;
27
+
28
+ export type ItemValue<T, TItemJson = unknown> = Readonly<{
29
+ readonly [ITEM_VALUE_BRAND]: true;
30
+ readonly fn: ItemValueCallback<T, TItemJson>;
31
+ }>;
32
+
33
+ export function itemValue<T, TItemJson = unknown>(fn: ItemValueCallback<T, TItemJson>): ItemValue<T, TItemJson> {
34
+ return { [ITEM_VALUE_BRAND]: true, fn };
35
+ }
36
+
37
+ export function isItemValue<T, TItemJson = unknown>(value: unknown): value is ItemValue<T, TItemJson> {
38
+ if (typeof value !== "object" || value === null) {
39
+ return false;
40
+ }
41
+ const v = value as Record<PropertyKey, unknown>;
42
+ if (v[ITEM_VALUE_BRAND] === true) {
43
+ return true;
44
+ }
45
+ // Support snapshot-hydrated itemValue wrappers where the symbol brand was lost but the callback survived.
46
+ // Workflow snapshot hydration currently restores function-valued fields (like `fn`) but may drop symbol-keyed brands.
47
+ // We treat the minimal `{ fn: Function }` shape as an itemValue wrapper to keep runnable configs working.
48
+ const keys = Object.keys(v);
49
+ if (keys.length === 1 && keys[0] === "fn" && typeof (v as { fn?: unknown }).fn === "function") {
50
+ return true;
51
+ }
52
+ // Support legacy module-local Symbol("codemation.itemValue") brands (e.g. duplicate module graphs).
53
+ for (const sym of Object.getOwnPropertySymbols(v)) {
54
+ if (sym.description === "codemation.itemValue" && v[sym] === true) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function containsItemValueInUnknown(value: unknown, seen: WeakSet<object> = new WeakSet()): boolean {
62
+ if (isItemValue(value)) {
63
+ return true;
64
+ }
65
+ if (value === null || typeof value !== "object") {
66
+ return false;
67
+ }
68
+ if (seen.has(value as object)) {
69
+ return false;
70
+ }
71
+ seen.add(value as object);
72
+ if (Array.isArray(value)) {
73
+ return value.some((entry) => containsItemValueInUnknown(entry, seen));
74
+ }
75
+ for (const entry of Object.values(value as Record<string, unknown>)) {
76
+ if (containsItemValueInUnknown(entry, seen)) {
77
+ return true;
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Deep-resolves {@link itemValue} leaves. Returns a new graph (does not mutate the original config object).
85
+ */
86
+ export async function resolveItemValuesInUnknown(
87
+ value: unknown,
88
+ args: ItemValueArgs,
89
+ seen: WeakSet<object> = new WeakSet(),
90
+ ): Promise<unknown> {
91
+ if (isItemValue(value)) {
92
+ return await Promise.resolve(value.fn(args));
93
+ }
94
+ if (value === null || typeof value !== "object") {
95
+ return value;
96
+ }
97
+ if (seen.has(value as object)) {
98
+ return value;
99
+ }
100
+ seen.add(value as object);
101
+ if (Array.isArray(value)) {
102
+ const out: unknown[] = [];
103
+ for (let i = 0; i < value.length; i++) {
104
+ out.push(await resolveItemValuesInUnknown(value[i], args, seen));
105
+ }
106
+ return out;
107
+ }
108
+ const rec = value as Record<string, unknown>;
109
+ const entries = Object.entries(rec);
110
+ const proto = Object.getPrototypeOf(value);
111
+ if (proto !== Object.prototype && proto !== null && entries.length === 0) {
112
+ return value;
113
+ }
114
+ const out = Object.create(proto) as Record<string, unknown>;
115
+ for (const [k, v] of entries) {
116
+ out[k] = await resolveItemValuesInUnknown(v, args, seen);
117
+ }
118
+ return out;
119
+ }
120
+
121
+ /**
122
+ * Clones runnable config (best-effort) so per-item {@link itemValue} resolution never mutates shared instances.
123
+ */
124
+ export async function resolveItemValuesForExecution(
125
+ config: unknown,
126
+ nodeCtx: NodeExecutionContext,
127
+ item: Item,
128
+ itemIndex: number,
129
+ items: Items,
130
+ ): Promise<unknown | undefined> {
131
+ const ivArgs: ItemValueArgs = {
132
+ item,
133
+ itemIndex,
134
+ items,
135
+ ctx: {
136
+ runId: nodeCtx.runId,
137
+ workflowId: nodeCtx.workflowId,
138
+ nodeId: nodeCtx.nodeId,
139
+ activationId: nodeCtx.activationId,
140
+ data: nodeCtx.data,
141
+ },
142
+ };
143
+ if (!containsItemValueInUnknown(config)) {
144
+ return undefined;
145
+ }
146
+ return await resolveItemValuesInUnknown(config, ivArgs);
147
+ }
@@ -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