@codemation/core 0.5.0 → 0.7.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 (63) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/{EngineRuntimeRegistration.types-BtTZolK0.d.ts → EngineRuntimeRegistration.types-_M7KFD3D.d.ts} +2 -2
  3. package/dist/{EngineWorkflowRunnerService-Ddl0fekp.d.cts → EngineWorkflowRunnerService-D0Cwngv7.d.cts} +2 -2
  4. package/dist/{InMemoryRunDataFactory-i-u2yngD.d.cts → InMemoryRunDataFactory-BIWx6e02.d.cts} +15 -6
  5. package/dist/{RunIntentService-Cjx-glgz.d.cts → RunIntentService-5k0p-J67.d.cts} +31 -12
  6. package/dist/{RunIntentService-Dkr4YwN8.d.ts → RunIntentService-CuXAIO6_.d.ts} +52 -28
  7. package/dist/bootstrap/index.cjs +2 -2
  8. package/dist/bootstrap/index.d.cts +6 -6
  9. package/dist/bootstrap/index.d.ts +3 -3
  10. package/dist/bootstrap/index.js +2 -2
  11. package/dist/{bootstrap-DbUlOl11.js → bootstrap-BhYxSivA.js} +5 -4
  12. package/dist/bootstrap-BhYxSivA.js.map +1 -0
  13. package/dist/{bootstrap-DHH2uo-W.cjs → bootstrap-D-TDU9Lu.cjs} +5 -4
  14. package/dist/bootstrap-D-TDU9Lu.cjs.map +1 -0
  15. package/dist/{index-B2v4wtys.d.ts → index-BnJ7_IrO.d.ts} +92 -13
  16. package/dist/index.cjs +94 -10
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +98 -23
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +84 -6
  21. package/dist/index.js.map +1 -1
  22. package/dist/{runtime-feFn8OmG.cjs → runtime-3YVDd2vY.cjs} +81 -73
  23. package/dist/runtime-3YVDd2vY.cjs.map +1 -0
  24. package/dist/{runtime-BdH94eBR.js → runtime-CJnObwsU.js} +66 -64
  25. package/dist/runtime-CJnObwsU.js.map +1 -0
  26. package/dist/testing.cjs +2 -2
  27. package/dist/testing.cjs.map +1 -1
  28. package/dist/testing.d.cts +2 -2
  29. package/dist/testing.d.ts +2 -2
  30. package/dist/testing.js +2 -2
  31. package/dist/testing.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/ai/AgentConfigInspectorFactory.ts +2 -2
  34. package/src/ai/AgentMessageConfigNormalizerFactory.ts +3 -3
  35. package/src/ai/AiHost.ts +22 -2
  36. package/src/ai/CallableToolConfig.ts +84 -0
  37. package/src/ai/CallableToolFactory.ts +13 -0
  38. package/src/ai/CallableToolKindToken.ts +5 -0
  39. package/src/authoring/callableTool.types.ts +12 -0
  40. package/src/authoring/defineNode.types.ts +38 -9
  41. package/src/authoring/index.ts +2 -0
  42. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +12 -4
  43. package/src/contracts/credentialTypes.ts +20 -0
  44. package/src/contracts/index.ts +2 -1
  45. package/src/contracts/{itemValue.ts → itemExpr.ts} +31 -32
  46. package/src/contracts/params.ts +10 -0
  47. package/src/contracts/runtimeTypes.ts +4 -2
  48. package/src/contracts/workflowTypes.ts +11 -9
  49. package/src/execution/{ItemValueResolver.ts → ItemExprResolver.ts} +5 -5
  50. package/src/execution/NodeExecutor.ts +13 -31
  51. package/src/execution/NodeExecutorFactory.ts +7 -2
  52. package/src/execution/NodeOutputNormalizer.ts +22 -23
  53. package/src/execution/RunnableOutputBehaviorResolver.ts +23 -0
  54. package/src/execution/index.ts +2 -1
  55. package/src/index.ts +2 -1
  56. package/src/runStorage/InMemoryRunData.ts +9 -5
  57. package/src/testing/SwitchHarnessNode.ts +0 -1
  58. package/src/types/index.ts +2 -1
  59. package/src/workflowSnapshots/WorkflowSnapshotCodec.ts +1 -1
  60. package/dist/bootstrap-DHH2uo-W.cjs.map +0 -1
  61. package/dist/bootstrap-DbUlOl11.js.map +0 -1
  62. package/dist/runtime-BdH94eBR.js.map +0 -1
  63. package/dist/runtime-feFn8OmG.cjs.map +0 -1
@@ -4,6 +4,7 @@ import type {
4
4
  CredentialRequirement,
5
5
  CredentialTypeId,
6
6
  } from "../contracts/credentialTypes";
7
+ import type { ParamDeep } from "../contracts/params";
7
8
  import type { RunnableNode, RunnableNodeExecuteArgs, NodeExecutionContext } from "../contracts/runtimeTypes";
8
9
  import type { Item, Items, RunnableNodeConfig } from "../contracts/workflowTypes";
9
10
  import type { TypeToken } from "../di";
@@ -67,6 +68,11 @@ export type DefineNodeExecuteArgs<TConfig extends CredentialJsonRecord, TInputJs
67
68
  ctx: NodeExecutionContext<RunnableNodeConfig<TInputJson, unknown> & Readonly<{ config: TConfig }>>;
68
69
  }>;
69
70
 
71
+ export type DefinedNodeConfigInput<TConfigResolved extends CredentialJsonRecord, TItemJson> = ParamDeep<
72
+ TConfigResolved,
73
+ TItemJson
74
+ >;
75
+
70
76
  export interface DefinedNode<
71
77
  TKey extends string,
72
78
  TConfig extends CredentialJsonRecord,
@@ -78,7 +84,11 @@ export interface DefinedNode<
78
84
  readonly key: TKey;
79
85
  readonly title: string;
80
86
  readonly description?: string;
81
- create(config: TConfig, name?: string, id?: string): RunnableNodeConfig<TInputJson, TOutputJson>;
87
+ create<TConfigItemJson = TInputJson>(
88
+ config: DefinedNodeConfigInput<TConfig, TConfigItemJson>,
89
+ name?: string,
90
+ id?: string,
91
+ ): RunnableNodeConfig<TInputJson, TOutputJson>;
82
92
  register(context: { registerNode<TValue>(token: TypeToken<TValue>, implementation?: TypeToken<TValue>): void }): void;
83
93
  }
84
94
 
@@ -108,6 +118,8 @@ export interface DefineNodeOptions<
108
118
  * Validates **`input`** (engine also accepts `inputSchema` on the node class).
109
119
  */
110
120
  readonly inputSchema?: ZodType<TInputJson>;
121
+ /** Preserve inbound `item.binary` when `execute` returns plain JSON or item-shaped results without `binary`. */
122
+ readonly keepBinaries?: boolean;
111
123
  execute(
112
124
  args: DefineNodeExecuteArgs<TConfig, TInputJson>,
113
125
  context: DefinedNodeRunContext<TConfig, TBindings>,
@@ -246,12 +258,17 @@ export function defineNode<
246
258
  readonly type: TypeToken<unknown> = DefinedNodeRuntime;
247
259
  readonly icon = options.icon;
248
260
  readonly inputSchema = options.inputSchema;
261
+ readonly keepBinaries = options.keepBinaries ?? false;
249
262
 
250
263
  constructor(
251
264
  public readonly name: string,
252
- public readonly config: TConfig,
265
+ config: DefinedNodeConfigInput<TConfig, unknown>,
253
266
  public readonly id?: string,
254
- ) {}
267
+ ) {
268
+ this.config = config as unknown as TConfig;
269
+ }
270
+
271
+ public readonly config: TConfig;
255
272
 
256
273
  getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
257
274
  return credentialRequirements;
@@ -263,8 +280,12 @@ export function defineNode<
263
280
  key: options.key,
264
281
  title: options.title,
265
282
  description: options.description,
266
- create(config, name = options.title, id) {
267
- return new DefinedRunnableNodeConfig(name, config, id);
283
+ create<TConfigItemJson = TInputJson>(
284
+ config: DefinedNodeConfigInput<TConfig, TConfigItemJson>,
285
+ name = options.title,
286
+ id?: string,
287
+ ) {
288
+ return new DefinedRunnableNodeConfig(name, config as DefinedNodeConfigInput<TConfig, unknown>, id);
268
289
  },
269
290
  register(context) {
270
291
  context.registerNode(DefinedNodeRuntime);
@@ -324,9 +345,13 @@ export function defineBatchNode<
324
345
 
325
346
  constructor(
326
347
  public readonly name: string,
327
- public readonly config: TConfig,
348
+ config: DefinedNodeConfigInput<TConfig, unknown>,
328
349
  public readonly id?: string,
329
- ) {}
350
+ ) {
351
+ this.config = config as unknown as TConfig;
352
+ }
353
+
354
+ public readonly config: TConfig;
330
355
 
331
356
  getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
332
357
  return credentialRequirements;
@@ -338,8 +363,12 @@ export function defineBatchNode<
338
363
  key: options.key,
339
364
  title: options.title,
340
365
  description: options.description,
341
- create(config, name = options.title, id) {
342
- return new DefinedRunnableNodeConfig(name, config, id);
366
+ create<TConfigItemJson = TInputJson>(
367
+ config: DefinedNodeConfigInput<TConfig, TConfigItemJson>,
368
+ name = options.title,
369
+ id?: string,
370
+ ) {
371
+ return new DefinedRunnableNodeConfig(name, config as DefinedNodeConfigInput<TConfig, unknown>, id);
343
372
  },
344
373
  register(context) {
345
374
  context.registerNode(DefinedNodeRuntime);
@@ -1,6 +1,7 @@
1
1
  export { DefinedNodeRegistry } from "./DefinedNodeRegistry";
2
2
  export type {
3
3
  DefinedNode,
4
+ DefinedNodeConfigInput,
4
5
  DefinedNodeCredentialAccessors,
5
6
  DefinedNodeCredentialBinding,
6
7
  DefinedNodeCredentialBindings,
@@ -12,3 +13,4 @@ export type {
12
13
  export { defineBatchNode, defineNode } from "./defineNode.types";
13
14
  export type { DefineCredentialOptions } from "./defineCredential.types";
14
15
  export { defineCredential } from "./defineCredential.types";
16
+ export { callableTool } from "./callableTool.types";
@@ -4,11 +4,12 @@ import { EngineExecutionLimitsPolicyFactory } from "../../policies/executionLimi
4
4
  import {
5
5
  DefaultAsyncSleeper,
6
6
  InProcessRetryRunnerFactory,
7
- ItemValueResolver,
7
+ ItemExprResolver,
8
8
  NodeExecutor,
9
9
  NodeExecutorFactory,
10
10
  NodeInstanceFactoryFactory,
11
11
  NodeOutputNormalizer,
12
+ RunnableOutputBehaviorResolver,
12
13
  } from "../../execution";
13
14
  import {
14
15
  EngineFactory,
@@ -40,12 +41,15 @@ export class EngineRuntimeRegistrar {
40
41
  }
41
42
 
42
43
  private registerSupportFactories(container: DependencyContainer): void {
43
- if (!container.isRegistered(ItemValueResolver, true)) {
44
- container.registerSingleton(ItemValueResolver, ItemValueResolver);
44
+ if (!container.isRegistered(ItemExprResolver, true)) {
45
+ container.registerSingleton(ItemExprResolver, ItemExprResolver);
45
46
  }
46
47
  if (!container.isRegistered(NodeOutputNormalizer, true)) {
47
48
  container.registerSingleton(NodeOutputNormalizer, NodeOutputNormalizer);
48
49
  }
50
+ if (!container.isRegistered(RunnableOutputBehaviorResolver, true)) {
51
+ container.registerSingleton(RunnableOutputBehaviorResolver, RunnableOutputBehaviorResolver);
52
+ }
49
53
  container.register(EngineExecutionLimitsPolicyFactory, { useClass: EngineExecutionLimitsPolicyFactory });
50
54
  container.register(NodeInstanceFactoryFactory, { useClass: NodeInstanceFactoryFactory });
51
55
  container.register(DefaultAsyncSleeper, { useClass: DefaultAsyncSleeper });
@@ -101,7 +105,11 @@ export class EngineRuntimeRegistrar {
101
105
  .create(dependencyContainer.resolve(DefaultAsyncSleeper));
102
106
  return dependencyContainer
103
107
  .resolve(NodeExecutorFactory)
104
- .create(dependencyContainer.resolve(CoreTokens.WorkflowNodeInstanceFactory), retryRunner);
108
+ .create(
109
+ dependencyContainer.resolve(CoreTokens.WorkflowNodeInstanceFactory),
110
+ retryRunner,
111
+ dependencyContainer.resolve(RunnableOutputBehaviorResolver),
112
+ );
105
113
  }),
106
114
  });
107
115
  }
@@ -13,6 +13,12 @@ export type CredentialFieldSchema = Readonly<{
13
13
  type: "string" | "password" | "textarea" | "json" | "boolean";
14
14
  required?: true;
15
15
  order?: number;
16
+ /**
17
+ * Where this field appears in the credential dialog. Use `"advanced"` for optional or
18
+ * power-user fields; they render inside a collapsible section (see `CredentialTypeDefinition.advancedSection`).
19
+ * Defaults to `"default"` when omitted.
20
+ */
21
+ visibility?: "default" | "advanced";
16
22
  placeholder?: string;
17
23
  helpText?: string;
18
24
  /** When set, host resolves this field from process.env at runtime; env wins over stored values. */
@@ -89,12 +95,26 @@ export type CredentialOAuth2AuthDefinition = Readonly<
89
95
 
90
96
  export type CredentialAuthDefinition = CredentialOAuth2AuthDefinition;
91
97
 
98
+ export type CredentialAdvancedSectionPresentation = Readonly<{
99
+ /** Collapsible section title (default: "Advanced"). */
100
+ title?: string;
101
+ /** Optional short helper text shown inside the section (above the fields). */
102
+ description?: string;
103
+ /** When true, the advanced section starts expanded. Default: false (collapsed). */
104
+ defaultOpen?: boolean;
105
+ }>;
106
+
92
107
  export type CredentialTypeDefinition = Readonly<{
93
108
  typeId: CredentialTypeId;
94
109
  displayName: string;
95
110
  description?: string;
96
111
  publicFields?: ReadonlyArray<CredentialFieldSchema>;
97
112
  secretFields?: ReadonlyArray<CredentialFieldSchema>;
113
+ /**
114
+ * Optional labels for the collapsible block that contains every field with `visibility: "advanced"`.
115
+ * If omitted, the UI still shows that block with defaults (title "Advanced", collapsed).
116
+ */
117
+ advancedSection?: CredentialAdvancedSectionPresentation;
98
118
  supportedSourceKinds?: ReadonlyArray<CredentialMaterialSourceKind>;
99
119
  auth?: CredentialAuthDefinition;
100
120
  }>;
@@ -2,7 +2,8 @@ export * from "./credentialTypes";
2
2
  export * from "./emitPorts";
3
3
  export * from "./executionPersistenceContracts";
4
4
  export * from "./itemMeta";
5
- export * from "./itemValue";
5
+ export * from "./params";
6
+ export * from "./itemExpr";
6
7
  export * from "./runtimeTypes";
7
8
  export * from "./runFinishedAtFactory";
8
9
  export * from "./runTypes";
@@ -1,9 +1,9 @@
1
1
  import type { NodeExecutionContext } from "./runtimeTypes";
2
2
  import type { Item, Items, NodeActivationId, NodeId, RunDataSnapshot, RunId, WorkflowId } from "./workflowTypes";
3
3
 
4
- const ITEM_VALUE_BRAND = Symbol.for("codemation.itemValue");
4
+ const ITEM_EXPR_BRAND = Symbol.for("codemation.itemExpr");
5
5
 
6
- export type ItemValueResolvedContext = Readonly<{
6
+ export type ItemExprResolvedContext = Readonly<{
7
7
  runId: RunId;
8
8
  workflowId: WorkflowId;
9
9
  nodeId: NodeId;
@@ -14,52 +14,51 @@ export type ItemValueResolvedContext = Readonly<{
14
14
  /**
15
15
  * Context aligned with former {@link ItemInputMapperContext} — use **`data`** to read any completed upstream node.
16
16
  */
17
- export type ItemValueContext = ItemValueResolvedContext;
17
+ export type ItemExprContext = ItemExprResolvedContext;
18
18
 
19
- export type ItemValueArgs<TItemJson = unknown> = Readonly<{
19
+ export type ItemExprArgs<TItemJson = unknown> = Readonly<{
20
20
  item: Item<TItemJson>;
21
21
  itemIndex: number;
22
22
  items: Items<TItemJson>;
23
- ctx: ItemValueContext;
23
+ ctx: ItemExprContext;
24
24
  }>;
25
25
 
26
- export type ItemValueCallback<T, TItemJson = unknown> = (args: ItemValueArgs<TItemJson>) => T | Promise<T>;
26
+ export type ItemExprCallback<T, TItemJson = unknown> = (args: ItemExprArgs<TItemJson>) => T | Promise<T>;
27
27
 
28
- export type ItemValue<T, TItemJson = unknown> = Readonly<{
29
- readonly [ITEM_VALUE_BRAND]: true;
30
- readonly fn: ItemValueCallback<T, TItemJson>;
28
+ export type ItemExpr<T, TItemJson = unknown> = Readonly<{
29
+ readonly [ITEM_EXPR_BRAND]: true;
30
+ readonly fn: ItemExprCallback<T, TItemJson>;
31
31
  }>;
32
32
 
33
- export function itemValue<T, TItemJson = unknown>(fn: ItemValueCallback<T, TItemJson>): ItemValue<T, TItemJson> {
34
- return { [ITEM_VALUE_BRAND]: true, fn };
33
+ export function itemExpr<T, TItemJson = unknown>(fn: ItemExprCallback<T, TItemJson>): ItemExpr<T, TItemJson> {
34
+ return { [ITEM_EXPR_BRAND]: true, fn };
35
35
  }
36
36
 
37
- export function isItemValue<T, TItemJson = unknown>(value: unknown): value is ItemValue<T, TItemJson> {
37
+ export function isItemExpr<T, TItemJson = unknown>(value: unknown): value is ItemExpr<T, TItemJson> {
38
38
  if (typeof value !== "object" || value === null) {
39
39
  return false;
40
40
  }
41
41
  const v = value as Record<PropertyKey, unknown>;
42
- if (v[ITEM_VALUE_BRAND] === true) {
42
+ if (v[ITEM_EXPR_BRAND] === true) {
43
43
  return true;
44
44
  }
45
- // Support snapshot-hydrated itemValue wrappers where the symbol brand was lost but the callback survived.
45
+ // Support snapshot-hydrated itemExpr wrappers where the symbol brand was lost but the callback survived.
46
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.
47
+ // We treat the minimal `{ fn: Function }` shape as an itemExpr wrapper to keep runnable configs working.
48
48
  const keys = Object.keys(v);
49
49
  if (keys.length === 1 && keys[0] === "fn" && typeof (v as { fn?: unknown }).fn === "function") {
50
50
  return true;
51
51
  }
52
- // Support legacy module-local Symbol("codemation.itemValue") brands (e.g. duplicate module graphs).
53
52
  for (const sym of Object.getOwnPropertySymbols(v)) {
54
- if (sym.description === "codemation.itemValue" && v[sym] === true) {
53
+ if (sym.description === "codemation.itemExpr" && v[sym] === true) {
55
54
  return true;
56
55
  }
57
56
  }
58
57
  return false;
59
58
  }
60
59
 
61
- function containsItemValueInUnknown(value: unknown, seen: WeakSet<object> = new WeakSet()): boolean {
62
- if (isItemValue(value)) {
60
+ function containsItemExprInUnknown(value: unknown, seen: WeakSet<object> = new WeakSet()): boolean {
61
+ if (isItemExpr(value)) {
63
62
  return true;
64
63
  }
65
64
  if (value === null || typeof value !== "object") {
@@ -70,10 +69,10 @@ function containsItemValueInUnknown(value: unknown, seen: WeakSet<object> = new
70
69
  }
71
70
  seen.add(value as object);
72
71
  if (Array.isArray(value)) {
73
- return value.some((entry) => containsItemValueInUnknown(entry, seen));
72
+ return value.some((entry) => containsItemExprInUnknown(entry, seen));
74
73
  }
75
74
  for (const entry of Object.values(value as Record<string, unknown>)) {
76
- if (containsItemValueInUnknown(entry, seen)) {
75
+ if (containsItemExprInUnknown(entry, seen)) {
77
76
  return true;
78
77
  }
79
78
  }
@@ -81,14 +80,14 @@ function containsItemValueInUnknown(value: unknown, seen: WeakSet<object> = new
81
80
  }
82
81
 
83
82
  /**
84
- * Deep-resolves {@link itemValue} leaves. Returns a new graph (does not mutate the original config object).
83
+ * Deep-resolves {@link itemExpr} leaves. Returns a new graph (does not mutate the original config object).
85
84
  */
86
- export async function resolveItemValuesInUnknown(
85
+ export async function resolveItemExprsInUnknown(
87
86
  value: unknown,
88
- args: ItemValueArgs,
87
+ args: ItemExprArgs,
89
88
  seen: WeakSet<object> = new WeakSet(),
90
89
  ): Promise<unknown> {
91
- if (isItemValue(value)) {
90
+ if (isItemExpr(value)) {
92
91
  return await Promise.resolve(value.fn(args));
93
92
  }
94
93
  if (value === null || typeof value !== "object") {
@@ -101,7 +100,7 @@ export async function resolveItemValuesInUnknown(
101
100
  if (Array.isArray(value)) {
102
101
  const out: unknown[] = [];
103
102
  for (let i = 0; i < value.length; i++) {
104
- out.push(await resolveItemValuesInUnknown(value[i], args, seen));
103
+ out.push(await resolveItemExprsInUnknown(value[i], args, seen));
105
104
  }
106
105
  return out;
107
106
  }
@@ -113,22 +112,22 @@ export async function resolveItemValuesInUnknown(
113
112
  }
114
113
  const out = Object.create(proto) as Record<string, unknown>;
115
114
  for (const [k, v] of entries) {
116
- out[k] = await resolveItemValuesInUnknown(v, args, seen);
115
+ out[k] = await resolveItemExprsInUnknown(v, args, seen);
117
116
  }
118
117
  return out;
119
118
  }
120
119
 
121
120
  /**
122
- * Clones runnable config (best-effort) so per-item {@link itemValue} resolution never mutates shared instances.
121
+ * Clones runnable config (best-effort) so per-item {@link itemExpr} resolution never mutates shared instances.
123
122
  */
124
- export async function resolveItemValuesForExecution(
123
+ export async function resolveItemExprsForExecution(
125
124
  config: unknown,
126
125
  nodeCtx: NodeExecutionContext,
127
126
  item: Item,
128
127
  itemIndex: number,
129
128
  items: Items,
130
129
  ): Promise<unknown | undefined> {
131
- const ivArgs: ItemValueArgs = {
130
+ const exprArgs: ItemExprArgs = {
132
131
  item,
133
132
  itemIndex,
134
133
  items,
@@ -140,8 +139,8 @@ export async function resolveItemValuesForExecution(
140
139
  data: nodeCtx.data,
141
140
  },
142
141
  };
143
- if (!containsItemValueInUnknown(config)) {
142
+ if (!containsItemExprInUnknown(config)) {
144
143
  return undefined;
145
144
  }
146
- return await resolveItemValuesInUnknown(config, ivArgs);
145
+ return await resolveItemExprsInUnknown(config, exprArgs);
147
146
  }
@@ -0,0 +1,10 @@
1
+ import type { ItemExpr } from "./itemExpr";
2
+
3
+ export type Expr<T, TItemJson = unknown> = ItemExpr<T, TItemJson>;
4
+
5
+ export type Param<T, TItemJson = unknown> = T | Expr<T, TItemJson>;
6
+
7
+ export type ParamDeep<T, TItemJson = unknown> =
8
+ | Expr<T, TItemJson>
9
+ | (T extends readonly (infer U)[] ? ReadonlyArray<ParamDeep<U, TItemJson>> : never)
10
+ | (T extends object ? { [K in keyof T]: ParamDeep<T[K], TItemJson> } : T);
@@ -223,8 +223,10 @@ export interface EngineHost {
223
223
  }
224
224
 
225
225
  /**
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).
226
+ * Per-item runnable node: return JSON, an array to fan-out on `main`, an explicit `Item`, or {@link emitPorts}
227
+ * for multi-port emission. Engine applies `inputSchema.parse(item.json)` and passes the result as `args.input`
228
+ * (wire `item.json` is unchanged). Transform helpers may opt into binary preservation, while routers and
229
+ * pass-through nodes should return explicit items when they need to preserve full item state.
228
230
  */
229
231
  export interface RunnableNodeExecuteArgs<
230
232
  TConfig extends RunnableNodeConfig<any, any> = RunnableNodeConfig<any, any>,
@@ -6,6 +6,7 @@ import type { RetryPolicySpec } from "./retryPolicySpec.types";
6
6
 
7
7
  export type WorkflowId = string;
8
8
  export type NodeId = string;
9
+ export type NodeIdRef<TJson = unknown> = NodeId & Readonly<{ __codemationNodeJson?: TJson }>;
9
10
  export type OutputPortKey = string;
10
11
  export type InputPortKey = string;
11
12
  export type PersistedTokenId = string;
@@ -96,8 +97,6 @@ export declare const runnableNodeInputType: unique symbol;
96
97
  export declare const runnableNodeOutputType: unique symbol;
97
98
  export declare const triggerNodeOutputType: unique symbol;
98
99
 
99
- export type LineageCarryPolicy = "emitOnly" | "carryThrough";
100
-
101
100
  /**
102
101
  * Runnable node: **`TInputJson`** is what **`inputSchema`** validates on **`item.json`** (the wire payload).
103
102
  * **`TOutputJson`** is emitted `item.json` on outputs.
@@ -111,11 +110,6 @@ export interface RunnableNodeConfig<TInputJson = unknown, TOutputJson = unknown>
111
110
  * Resolution order: node instance `inputSchema`, then config `inputSchema`, then `z.unknown()`.
112
111
  */
113
112
  readonly inputSchema?: ZodType<TInputJson>;
114
- /**
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
113
  /**
120
114
  * When an activation receives **zero** input items, the engine normally runs `execute` zero times.
121
115
  * Set to **`runOnce`** to run `execute` once with an empty `items` batch (and a synthetic wire item for schema parsing).
@@ -161,6 +155,10 @@ export interface NodeRef {
161
155
  name?: string;
162
156
  }
163
157
 
158
+ export function nodeRef<TJson>(nodeId: NodeId): NodeIdRef<TJson> {
159
+ return nodeId as NodeIdRef<TJson>;
160
+ }
161
+
164
162
  export type PairedItemRef = Readonly<{ nodeId: NodeId; output: OutputPortKey; itemIndex: number }>;
165
163
 
166
164
  export type BinaryPreviewKind = "image" | "audio" | "video" | "download";
@@ -211,8 +209,12 @@ export interface ParentExecutionRef {
211
209
 
212
210
  export interface RunDataSnapshot {
213
211
  getOutputs(nodeId: NodeId): NodeOutputs | undefined;
214
- getOutputItems(nodeId: NodeId, output?: OutputPortKey): Items;
215
- getOutputItem(nodeId: NodeId, itemIndex: number, output?: OutputPortKey): Item | undefined;
212
+ getOutputItems<TJson = unknown>(nodeId: NodeId | NodeIdRef<TJson>, output?: OutputPortKey): Items<TJson>;
213
+ getOutputItem<TJson = unknown>(
214
+ nodeId: NodeId | NodeIdRef<TJson>,
215
+ itemIndex: number,
216
+ output?: OutputPortKey,
217
+ ): Item<TJson> | undefined;
216
218
  }
217
219
 
218
220
  export interface MutableRunData extends RunDataSnapshot {
@@ -1,10 +1,10 @@
1
- import { resolveItemValuesForExecution } from "../contracts/itemValue";
1
+ import { resolveItemExprsForExecution } from "../contracts/itemExpr";
2
2
  import type { Item, NodeExecutionContext, RunnableNodeConfig } from "../types";
3
3
 
4
4
  /**
5
- * Resolves {@link import("../contracts/itemValue").ItemValue} leaves on runnable config before {@link RunnableNode.execute}.
5
+ * Resolves {@link import("../contracts/itemExpr").ItemExpr} leaves on runnable config before {@link RunnableNode.execute}.
6
6
  */
7
- export class ItemValueResolver {
7
+ export class ItemExprResolver {
8
8
  async resolveConfigForItem<TConfig extends RunnableNodeConfig<any, any>>(
9
9
  ctx: NodeExecutionContext<TConfig>,
10
10
  item: Item,
@@ -12,9 +12,9 @@ export class ItemValueResolver {
12
12
  items: ReadonlyArray<Item>,
13
13
  ): Promise<NodeExecutionContext<TConfig>> {
14
14
  if (!ctx) {
15
- throw new Error("ItemValueResolver.resolveConfigForItem: ctx is required");
15
+ throw new Error("ItemExprResolver.resolveConfigForItem: ctx is required");
16
16
  }
17
- const resolvedConfig = await resolveItemValuesForExecution(ctx.config, ctx, item, itemIndex, items);
17
+ const resolvedConfig = await resolveItemExprsForExecution(ctx.config, ctx, item, itemIndex, items);
18
18
  const merged = resolvedConfig !== undefined && resolvedConfig !== null ? resolvedConfig : ctx.config;
19
19
  if (merged === undefined || merged === null) {
20
20
  return ctx;
@@ -3,10 +3,8 @@ import { isPortsEmission, isUnbrandedPortsEmissionShape } from "../contracts/emi
3
3
 
4
4
  import type {
5
5
  Item,
6
- LineageCarryPolicy,
7
6
  MultiInputNode,
8
7
  NodeActivationRequest,
9
- NodeConfigBase,
10
8
  NodeExecutionContext,
11
9
  NodeOutputs,
12
10
  RunnableNode,
@@ -17,21 +15,25 @@ import type {
17
15
  } from "../types";
18
16
 
19
17
  import { FanInMergeByOriginMerger } from "./FanInMergeByOriginMerger";
20
- import { ItemValueResolver } from "./ItemValueResolver";
18
+ import { ItemExprResolver } from "./ItemExprResolver";
21
19
  import { InProcessRetryRunner } from "./InProcessRetryRunner";
22
20
  import { NodeOutputNormalizer } from "./NodeOutputNormalizer";
21
+ import { RunnableOutputBehaviorResolver } from "./RunnableOutputBehaviorResolver";
23
22
 
24
23
  export class NodeExecutor {
25
24
  private readonly fanInMerger = new FanInMergeByOriginMerger();
26
25
  private readonly outputNormalizer = new NodeOutputNormalizer();
27
- private readonly itemValueResolver: ItemValueResolver;
26
+ private readonly itemExprResolver: ItemExprResolver;
27
+ private readonly outputBehaviorResolver: RunnableOutputBehaviorResolver;
28
28
 
29
29
  constructor(
30
30
  private readonly nodeInstanceFactory: WorkflowNodeInstanceFactory,
31
31
  private readonly retryRunner: InProcessRetryRunner,
32
- itemValueResolver?: ItemValueResolver,
32
+ itemExprResolver?: ItemExprResolver,
33
+ outputBehaviorResolver?: RunnableOutputBehaviorResolver,
33
34
  ) {
34
- this.itemValueResolver = itemValueResolver ?? new ItemValueResolver();
35
+ this.itemExprResolver = itemExprResolver ?? new ItemExprResolver();
36
+ this.outputBehaviorResolver = outputBehaviorResolver ?? new RunnableOutputBehaviorResolver();
35
37
  }
36
38
 
37
39
  async execute(request: NodeActivationRequest): Promise<NodeOutputs> {
@@ -126,14 +128,14 @@ export class NodeExecutor {
126
128
  node: RunnableNode,
127
129
  ): Promise<NodeOutputs> {
128
130
  const runnableConfig = request.ctx.config as RunnableNodeConfig;
129
- const carry = this.resolveLineageCarry(node, runnableConfig);
131
+ const behavior = this.outputBehaviorResolver.resolve(runnableConfig);
130
132
  const inputSchema = this.resolveInputSchema(node, runnableConfig);
131
133
  const inputBatch = request.input ?? [];
132
134
  if (inputBatch.length === 0 && runnableConfig.emptyBatchExecution === "runOnce") {
133
135
  const syntheticItem: Item = { json: {} };
134
136
  const parsed = inputSchema.parse(syntheticItem.json);
135
137
  const runnableCtx = request.ctx as NodeExecutionContext<RunnableNodeConfig>;
136
- const resolvedCtx = await this.itemValueResolver.resolveConfigForItem(runnableCtx, syntheticItem, 0, inputBatch);
138
+ const resolvedCtx = await this.itemExprResolver.resolveConfigForItem(runnableCtx, syntheticItem, 0, inputBatch);
137
139
  const ctx = this.pickExecutionContext(runnableCtx, resolvedCtx);
138
140
  const args: RunnableNodeExecuteArgs = {
139
141
  input: parsed,
@@ -146,7 +148,7 @@ export class NodeExecutor {
146
148
  return this.outputNormalizer.normalizeExecuteResult({
147
149
  baseItem: syntheticItem,
148
150
  raw,
149
- carry,
151
+ behavior,
150
152
  }) as NodeOutputs;
151
153
  }
152
154
  const byPort: Partial<Record<string, Item[]>> = {};
@@ -155,7 +157,7 @@ export class NodeExecutor {
155
157
  this.assertItemJsonNotTopLevelArray(request.nodeId, item);
156
158
  const parsed = inputSchema.parse(item.json);
157
159
  const runnableCtx = request.ctx as NodeExecutionContext<RunnableNodeConfig>;
158
- const resolvedCtx = await this.itemValueResolver.resolveConfigForItem(runnableCtx, item, i, inputBatch);
160
+ const resolvedCtx = await this.itemExprResolver.resolveConfigForItem(runnableCtx, item, i, inputBatch);
159
161
  const ctx = this.pickExecutionContext(runnableCtx, resolvedCtx);
160
162
  const args: RunnableNodeExecuteArgs = {
161
163
  input: parsed,
@@ -168,7 +170,7 @@ export class NodeExecutor {
168
170
  const normalized = this.outputNormalizer.normalizeExecuteResult({
169
171
  baseItem: item,
170
172
  raw,
171
- carry,
173
+ behavior,
172
174
  });
173
175
  for (const [port, batch] of Object.entries(normalized)) {
174
176
  if (!batch || batch.length === 0) {
@@ -226,24 +228,4 @@ export class NodeExecutor {
226
228
  );
227
229
  }
228
230
  }
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";
248
- }
249
231
  }
@@ -2,9 +2,14 @@ import type { WorkflowNodeInstanceFactory } from "../types";
2
2
 
3
3
  import { InProcessRetryRunner } from "./InProcessRetryRunner";
4
4
  import { NodeExecutor } from "./NodeExecutor";
5
+ import { RunnableOutputBehaviorResolver } from "./RunnableOutputBehaviorResolver";
5
6
 
6
7
  export class NodeExecutorFactory {
7
- create(workflowNodeInstanceFactory: WorkflowNodeInstanceFactory, retryRunner: InProcessRetryRunner): NodeExecutor {
8
- return new NodeExecutor(workflowNodeInstanceFactory, retryRunner);
8
+ create(
9
+ workflowNodeInstanceFactory: WorkflowNodeInstanceFactory,
10
+ retryRunner: InProcessRetryRunner,
11
+ outputBehaviorResolver: RunnableOutputBehaviorResolver,
12
+ ): NodeExecutor {
13
+ return new NodeExecutor(workflowNodeInstanceFactory, retryRunner, undefined, outputBehaviorResolver);
9
14
  }
10
15
  }