@codemation/core-nodes 0.0.24 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/core-nodes",
3
- "version": "0.0.24",
3
+ "version": "0.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,11 +31,12 @@
31
31
  "@langchain/core": "^1.1.31",
32
32
  "@langchain/openai": "^1.2.12",
33
33
  "lucide-react": "^0.577.0",
34
- "@codemation/core": "0.2.3"
34
+ "@codemation/core": "0.4.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.3.5",
38
38
  "eslint": "^10.0.3",
39
+ "reflect-metadata": "^0.2.2",
39
40
  "tsdown": "^0.15.5",
40
41
  "tsx": "^4.21.0",
41
42
  "typescript": "^5.9.3",
@@ -43,6 +44,7 @@
43
44
  "zod": "^4.3.6"
44
45
  },
45
46
  "scripts": {
47
+ "changeset:verify": "pnpm --workspace-root run changeset:verify",
46
48
  "dev": "tsdown --watch",
47
49
  "build": "tsdown",
48
50
  "typecheck": "tsc -p tsconfig.json --noEmit",
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./nodes/httpRequest";
9
9
  export * from "./nodes/aggregate";
10
10
  export * from "./nodes/filter";
11
11
  export * from "./nodes/if";
12
+ export * from "./nodes/switch";
12
13
  export * from "./nodes/split";
13
14
  export * from "./nodes/ManualTriggerFactory";
14
15
  export * from "./nodes/mapData";
@@ -4,7 +4,6 @@ import {
4
4
  type AgentMessageConfig,
5
5
  type AgentNodeConfig,
6
6
  type ChatModelConfig,
7
- type ItemInputMapper,
8
7
  type RetryPolicySpec,
9
8
  type RunnableNodeConfig,
10
9
  type ToolConfig,
@@ -14,7 +13,7 @@ import type { ZodType } from "zod";
14
13
 
15
14
  import { AIAgentNode } from "./AIAgentNode";
16
15
 
17
- export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown, TWireJson = TInputJson> {
16
+ export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown> {
18
17
  readonly name: string;
19
18
  readonly messages: AgentMessageConfig<TInputJson>;
20
19
  readonly chatModel: ChatModelConfig;
@@ -22,18 +21,16 @@ export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown, TW
22
21
  readonly id?: string;
23
22
  readonly retryPolicy?: RetryPolicySpec;
24
23
  readonly guardrails?: AgentGuardrailConfig;
25
- /** Engine applies with {@link RunnableNodeConfig.inputSchema} before {@link AIAgentNode.executeOne}. */
24
+ /** Engine applies with {@link RunnableNodeConfig.inputSchema} before {@link AIAgentNode.execute}. */
26
25
  readonly inputSchema?: ZodType<TInputJson>;
27
- /** Per-item mapper before validation; use with {@link inputSchema} so persisted run inputs show the prompt payload. */
28
- readonly mapInput?: ItemInputMapper<TWireJson, TInputJson>;
29
26
  }
30
27
 
31
28
  /**
32
29
  * AI agent: credential bindings are keyed to connection-owned LLM/tool node ids (ConnectionNodeIdFactory),
33
30
  * not to the agent workflow node id.
34
31
  */
35
- export class AIAgent<TInputJson = unknown, TOutputJson = unknown, TWireJson = TInputJson>
36
- implements RunnableNodeConfig<TInputJson, TOutputJson, TWireJson>, AgentNodeConfig<TInputJson, TOutputJson, TWireJson>
32
+ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
33
+ implements RunnableNodeConfig<TInputJson, TOutputJson>, AgentNodeConfig<TInputJson, TOutputJson>
37
34
  {
38
35
  readonly kind = "node" as const;
39
36
  readonly type: TypeToken<unknown> = AIAgentNode;
@@ -47,9 +44,8 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown, TWireJson = TI
47
44
  readonly retryPolicy: RetryPolicySpec;
48
45
  readonly guardrails?: AgentGuardrailConfig;
49
46
  readonly inputSchema?: ZodType<TInputJson>;
50
- readonly mapInput?: ItemInputMapper<TWireJson, TInputJson>;
51
47
 
52
- constructor(options: AIAgentOptions<TInputJson, TOutputJson, TWireJson>) {
48
+ constructor(options: AIAgentOptions<TInputJson, TOutputJson>) {
53
49
  this.name = options.name;
54
50
  this.messages = options.messages;
55
51
  this.chatModel = options.chatModel;
@@ -58,6 +54,5 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown, TWireJson = TI
58
54
  this.retryPolicy = options.retryPolicy ?? RetryPolicy.defaultForAiAgent;
59
55
  this.guardrails = options.guardrails;
60
56
  this.inputSchema = options.inputSchema;
61
- this.mapInput = options.mapInput;
62
57
  }
63
58
  }
@@ -4,12 +4,13 @@ import type {
4
4
  ChatModelConfig,
5
5
  ChatModelFactory,
6
6
  Item,
7
- ItemNode,
8
7
  Items,
9
8
  JsonValue,
10
9
  LangChainChatModelLike,
11
10
  NodeExecutionContext,
12
11
  NodeInputsByPort,
12
+ RunnableNode,
13
+ RunnableNodeExecuteArgs,
13
14
  Tool,
14
15
  ToolConfig,
15
16
  ZodSchemaAny,
@@ -59,12 +60,13 @@ interface PreparedAgentExecution {
59
60
  }
60
61
 
61
62
  @node({ packageName: "@codemation/core-nodes" })
62
- export class AIAgentNode implements ItemNode<AIAgent<any, any>, unknown, unknown> {
63
+ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
63
64
  kind = "node" as const;
64
65
  outputPorts = ["main"] as const;
65
66
  /**
66
- * Engine applies {@link RunnableNodeConfig.mapInput} + parse before {@link #executeOne}. Prefer modeling
67
- * prompts as {@code { messages: [{ role, content }, ...] }} so persisted inputs are visible in the UI.
67
+ * Engine validates {@link RunnableNodeConfig.inputSchema} (Zod) on {@code item.json} before enqueue, then resolves
68
+ * per-item **`itemValue`** leaves on config before {@link #execute}. Prefer modeling prompts as
69
+ * {@code { messages: [{ role, content }, ...] }} (on input or config) so persisted inputs are visible in the UI.
68
70
  */
69
71
  readonly inputSchema = z.unknown();
70
72
 
@@ -89,13 +91,7 @@ export class AIAgentNode implements ItemNode<AIAgent<any, any>, unknown, unknown
89
91
  this.executionHelpers.createConnectionCredentialExecutionContextFactory(credentialSessions);
90
92
  }
91
93
 
92
- async executeOne(args: {
93
- input: unknown;
94
- item: Item;
95
- itemIndex: number;
96
- items: Items;
97
- ctx: NodeExecutionContext<AIAgent<any, any>>;
98
- }): Promise<unknown> {
94
+ async execute(args: RunnableNodeExecuteArgs<AIAgent<any, any>>): Promise<unknown> {
99
95
  const prepared = await this.getOrPrepareExecution(args.ctx);
100
96
  const itemWithMappedJson = { ...args.item, json: args.input };
101
97
  const resultItem = await this.runAgentForItem(prepared, itemWithMappedJson, args.itemIndex, args.items);
@@ -1,19 +1,18 @@
1
- import type { Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
1
+ import type { RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
2
 
3
3
  import { node } from "@codemation/core";
4
4
 
5
5
  import type { Aggregate } from "./aggregate";
6
6
 
7
7
  @node({ packageName: "@codemation/core-nodes" })
8
- export class AggregateNode implements Node<Aggregate<any, any>> {
8
+ export class AggregateNode implements RunnableNode<Aggregate<any, any>> {
9
9
  kind = "node" as const;
10
10
  outputPorts = ["main"] as const;
11
11
 
12
- async execute(items: Items, ctx: NodeExecutionContext<Aggregate<any, any>>): Promise<NodeOutputs> {
13
- if (items.length === 0) {
14
- return { main: [] };
12
+ async execute(args: RunnableNodeExecuteArgs<Aggregate<any, any>>): Promise<unknown> {
13
+ if (args.itemIndex !== args.items.length - 1) {
14
+ return [];
15
15
  }
16
- const json = await Promise.resolve(ctx.config.aggregate(items as Items, ctx));
17
- return { main: [{ json }] };
16
+ return Promise.resolve(args.ctx.config.aggregate(args.items, args.ctx));
18
17
  }
19
18
  }
@@ -1,17 +1,29 @@
1
- import type { Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
2
-
1
+ import type { RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
3
2
  import { node } from "@codemation/core";
4
3
 
5
4
  import { Callback } from "./CallbackNodeFactory";
6
5
  import { CallbackResultNormalizer } from "./CallbackResultNormalizerFactory";
7
6
 
8
7
  @node({ packageName: "@codemation/core-nodes" })
9
- export class CallbackNode implements Node<Callback<any, any>> {
8
+ export class CallbackNode implements RunnableNode<Callback<any, any>> {
10
9
  kind = "node" as const;
11
10
  outputPorts = ["main"] as const;
12
11
 
13
- async execute(items: Items, ctx: NodeExecutionContext<Callback<any, any>>): Promise<NodeOutputs> {
14
- const result = await ctx.config.callback(items, ctx);
15
- return CallbackResultNormalizer.toNodeOutputs(result, items);
12
+ async execute(args: RunnableNodeExecuteArgs<Callback<any, any>>): Promise<unknown> {
13
+ const items = args.items ?? [];
14
+ const ctx = args.ctx;
15
+ const config = ctx.config;
16
+ if (config == null) {
17
+ throw new Error("CallbackNode: missing ctx.config (engine should always pass runnable config)");
18
+ }
19
+ if (items.length === 0) {
20
+ const result = await config.callback(items, ctx);
21
+ return CallbackResultNormalizer.toPortsEmission(result, items);
22
+ }
23
+ if (args.itemIndex !== items.length - 1) {
24
+ return [];
25
+ }
26
+ const result = await config.callback(items, ctx);
27
+ return CallbackResultNormalizer.toPortsEmission(result, items);
16
28
  }
17
29
  }
@@ -1,4 +1,12 @@
1
- import type { Items, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
1
+ import type {
2
+ Items,
3
+ NodeExecutionContext,
4
+ NodeErrorHandlerSpec,
5
+ PortsEmission,
6
+ RetryPolicySpec,
7
+ RunnableNodeConfig,
8
+ TypeToken,
9
+ } from "@codemation/core";
2
10
 
3
11
  import { CallbackNode } from "./CallbackNode";
4
12
 
@@ -9,7 +17,14 @@ export type CallbackHandler<
9
17
  > = (
10
18
  items: Items<TInputJson>,
11
19
  ctx: NodeExecutionContext<TConfig>,
12
- ) => Promise<Items<TOutputJson> | void> | Items<TOutputJson> | void;
20
+ ) => Promise<Items<TOutputJson> | PortsEmission | void> | Items<TOutputJson> | PortsEmission | void;
21
+
22
+ export type CallbackOptions = Readonly<{
23
+ id?: string;
24
+ retryPolicy?: RetryPolicySpec;
25
+ nodeErrorHandler?: NodeErrorHandlerSpec;
26
+ declaredOutputPorts?: ReadonlyArray<string>;
27
+ }>;
13
28
 
14
29
  export class Callback<TInputJson = unknown, TOutputJson = TInputJson> implements RunnableNodeConfig<
15
30
  TInputJson,
@@ -19,6 +34,11 @@ export class Callback<TInputJson = unknown, TOutputJson = TInputJson> implements
19
34
  readonly type: TypeToken<unknown> = CallbackNode;
20
35
  readonly execution = { hint: "local" } as const;
21
36
  readonly icon = "lucide:braces" as const;
37
+ readonly emptyBatchExecution = "runOnce" as const;
38
+ readonly id?: string;
39
+ readonly retryPolicy?: RetryPolicySpec;
40
+ readonly nodeErrorHandler?: NodeErrorHandlerSpec;
41
+ readonly declaredOutputPorts?: ReadonlyArray<string>;
22
42
 
23
43
  constructor(
24
44
  public readonly name: string = "Callback",
@@ -26,8 +46,15 @@ export class Callback<TInputJson = unknown, TOutputJson = TInputJson> implements
26
46
  TInputJson,
27
47
  TOutputJson
28
48
  >,
29
- public readonly id?: string,
30
- ) {}
49
+ idOrOptions?: string | CallbackOptions,
50
+ options?: CallbackOptions,
51
+ ) {
52
+ const resolvedOptions = typeof idOrOptions === "string" ? { ...options, id: idOrOptions } : idOrOptions;
53
+ this.id = resolvedOptions?.id;
54
+ this.retryPolicy = resolvedOptions?.retryPolicy;
55
+ this.nodeErrorHandler = resolvedOptions?.nodeErrorHandler;
56
+ this.declaredOutputPorts = resolvedOptions?.declaredOutputPorts;
57
+ }
31
58
 
32
59
  private static defaultCallback<TItemJson>(items: Items<TItemJson>): Items<TItemJson> {
33
60
  return items;
@@ -1,7 +1,11 @@
1
- import type { Items, NodeOutputs } from "@codemation/core";
1
+ import type { Items, PortsEmission } from "@codemation/core";
2
+ import { emitPorts, isPortsEmission } from "@codemation/core";
2
3
 
3
4
  export class CallbackResultNormalizer {
4
- static toNodeOutputs(result: Items | void, items: Items): NodeOutputs {
5
- return { main: result ?? items };
5
+ static toPortsEmission(result: Items | PortsEmission | void, items: Items): PortsEmission {
6
+ if (isPortsEmission(result)) {
7
+ return result;
8
+ }
9
+ return emitPorts({ main: result ?? items });
6
10
  }
7
11
  }
@@ -1,4 +1,4 @@
1
- import type { Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
1
+ import type { RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
2
  import { node } from "@codemation/core";
3
3
 
4
4
  import type { ConnectionCredentialNodeConfig } from "./ConnectionCredentialNodeConfig";
@@ -8,11 +8,11 @@ import type { ConnectionCredentialNodeConfig } from "./ConnectionCredentialNodeC
8
8
  * The engine does not schedule these; they exist for credentials, tokens, and UI identity.
9
9
  */
10
10
  @node({ packageName: "@codemation/core-nodes" })
11
- export class ConnectionCredentialNode implements Node<ConnectionCredentialNodeConfig> {
11
+ export class ConnectionCredentialNode implements RunnableNode<ConnectionCredentialNodeConfig> {
12
12
  kind = "node" as const;
13
13
  outputPorts = ["main"] as const;
14
14
 
15
- async execute(_items: Items, _ctx: NodeExecutionContext<ConnectionCredentialNodeConfig>): Promise<NodeOutputs> {
16
- return { main: [] };
15
+ execute(_args: RunnableNodeExecuteArgs<ConnectionCredentialNodeConfig>): unknown {
16
+ return [];
17
17
  }
18
18
  }
@@ -1,22 +1,18 @@
1
- import type { Item, Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
1
+ import type { Item, RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
2
 
3
3
  import { node } from "@codemation/core";
4
4
 
5
5
  import type { Filter } from "./filter";
6
6
 
7
7
  @node({ packageName: "@codemation/core-nodes" })
8
- export class FilterNode implements Node<Filter<any>> {
8
+ export class FilterNode implements RunnableNode<Filter<any>> {
9
9
  kind = "node" as const;
10
10
  outputPorts = ["main"] as const;
11
11
 
12
- async execute(items: Items, ctx: NodeExecutionContext<Filter<any>>): Promise<NodeOutputs> {
13
- const out: Item[] = [];
14
- for (let i = 0; i < items.length; i++) {
15
- const item = items[i] as Item;
16
- if (ctx.config.predicate(item as Item, i, items as Items, ctx)) {
17
- out.push(item);
18
- }
12
+ execute(args: RunnableNodeExecuteArgs<Filter<any>>): unknown {
13
+ if (args.ctx.config.predicate(args.item as Item, args.itemIndex, args.items, args.ctx)) {
14
+ return args.item;
19
15
  }
20
- return { main: out };
16
+ return [];
21
17
  }
22
18
  }
@@ -1,4 +1,4 @@
1
- import type { Item, Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
1
+ import type { Item, NodeExecutionContext, RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
2
 
3
3
  import { node } from "@codemation/core";
4
4
 
@@ -6,16 +6,12 @@ import type { HttpRequestDownloadMode } from "./httpRequest";
6
6
  import { HttpRequest } from "./httpRequest";
7
7
 
8
8
  @node({ packageName: "@codemation/core-nodes" })
9
- export class HttpRequestNode implements Node<HttpRequest<any, any>> {
9
+ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
10
10
  readonly kind = "node" as const;
11
11
  readonly outputPorts = ["main"] as const;
12
12
 
13
- async execute(items: Items, ctx: NodeExecutionContext<HttpRequest<any, any>>): Promise<NodeOutputs> {
14
- const output: Item[] = [];
15
- for (const item of items) {
16
- output.push(await this.executeItem(item, ctx));
17
- }
18
- return { main: output };
13
+ async execute(args: RunnableNodeExecuteArgs<HttpRequest<any, any>>): Promise<unknown> {
14
+ return await this.executeItem(args.item, args.ctx);
19
15
  }
20
16
 
21
17
  private async executeItem(item: Item, ctx: NodeExecutionContext<HttpRequest<any, any>>): Promise<Item> {
@@ -1,35 +1,23 @@
1
- import type { Item, Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
2
-
3
- import { node } from "@codemation/core";
1
+ import type { RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
+ import { emitPorts, node } from "@codemation/core";
4
3
 
5
4
  import { If } from "./if";
5
+ import { tagItemForRouterFanIn } from "./mergeExecutionUtils.types";
6
6
 
7
7
  @node({ packageName: "@codemation/core-nodes" })
8
- export class IfNode implements Node<If<any>> {
8
+ export class IfNode implements RunnableNode<If<any>> {
9
9
  kind = "node" as const;
10
- outputPorts = ["true", "false"] as const;
11
10
 
12
- async execute(items: Items, ctx: NodeExecutionContext<If<any>>): Promise<NodeOutputs> {
13
- const t: Item[] = [];
14
- const f: Item[] = [];
15
- for (let i = 0; i < items.length; i++) {
16
- const item = items[i] as Item<unknown>;
17
- const metaBase = (
18
- item.meta && typeof item.meta === "object" ? (item.meta as Record<string, unknown>) : {}
19
- ) as Record<string, unknown>;
20
- const cmBase =
21
- metaBase._cm && typeof metaBase._cm === "object"
22
- ? (metaBase._cm as Record<string, unknown>)
23
- : ({} as Record<string, unknown>);
24
- const originIndex = typeof cmBase.originIndex === "number" ? (cmBase.originIndex as number) : i;
25
- const tagged: Item = {
26
- ...item,
27
- meta: { ...metaBase, _cm: { ...cmBase, originIndex } },
28
- paired: [{ nodeId: ctx.nodeId, output: "$in", itemIndex: originIndex }, ...(item.paired ?? [])],
29
- };
30
- const ok = ctx.config.predicate(item, i, items, ctx);
31
- (ok ? t : f).push(tagged);
32
- }
33
- return { true: t, false: f };
11
+ execute(args: RunnableNodeExecuteArgs<If<any>>): unknown {
12
+ const tagged = tagItemForRouterFanIn({
13
+ item: args.item,
14
+ itemIndex: args.itemIndex,
15
+ nodeId: args.ctx.nodeId,
16
+ });
17
+ const ok = args.ctx.config.predicate(args.item, args.itemIndex, args.items, args.ctx);
18
+ return emitPorts({
19
+ true: ok ? [tagged] : [],
20
+ false: ok ? [] : [tagged],
21
+ });
34
22
  }
35
23
  }
@@ -1,4 +1,4 @@
1
- import type { Item, ItemNode, Items, NodeExecutionContext } from "@codemation/core";
1
+ import type { RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
2
 
3
3
  import { node } from "@codemation/core";
4
4
  import { z } from "zod";
@@ -6,18 +6,12 @@ import { z } from "zod";
6
6
  import { MapData } from "./mapData";
7
7
 
8
8
  @node({ packageName: "@codemation/core-nodes" })
9
- export class MapDataNode implements ItemNode<MapData<any, any>, unknown, unknown> {
9
+ export class MapDataNode implements RunnableNode<MapData<any, any>> {
10
10
  kind = "node" as const;
11
11
  outputPorts = ["main"] as const;
12
12
  readonly inputSchema = z.unknown();
13
13
 
14
- async executeOne(args: {
15
- input: unknown;
16
- item: Item;
17
- itemIndex: number;
18
- items: Items;
19
- ctx: NodeExecutionContext<MapData<any, any>>;
20
- }): Promise<unknown> {
14
+ async execute(args: RunnableNodeExecuteArgs<MapData<any, any>>): Promise<unknown> {
21
15
  return args.ctx.config.map(args.item, args.ctx);
22
16
  }
23
17
  }
@@ -1,15 +1,15 @@
1
- import type { Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
1
+ import type { RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
2
 
3
3
  import { node } from "@codemation/core";
4
4
 
5
5
  import { NoOp } from "./noOp";
6
6
 
7
7
  @node({ packageName: "@codemation/core-nodes" })
8
- export class NoOpNode implements Node<NoOp<any>> {
8
+ export class NoOpNode implements RunnableNode<NoOp<any>> {
9
9
  kind = "node" as const;
10
10
  outputPorts = ["main"] as const;
11
11
 
12
- async execute(items: Items, _ctx: NodeExecutionContext<NoOp<any>>): Promise<NodeOutputs> {
13
- return { main: items };
12
+ execute(args: RunnableNodeExecuteArgs<NoOp<any>>): unknown {
13
+ return args.item;
14
14
  }
15
15
  }
@@ -1,20 +1,27 @@
1
1
  import type {
2
2
  MultiInputNode,
3
- Node,
4
3
  NodeExecutionContext,
5
4
  NodeOutputs,
6
5
  NodeResolver,
6
+ RunnableNode,
7
+ RunnableNodeConfig,
8
+ RunnableNodeExecuteArgs,
7
9
  NodeBackedToolConfig,
8
10
  ToolExecuteArgs,
9
11
  ZodSchemaAny,
10
12
  } from "@codemation/core";
11
- import { CoreTokens, inject, injectable } from "@codemation/core";
13
+ import { CoreTokens, inject, injectable, ItemValueResolver, NodeOutputNormalizer } from "@codemation/core";
14
+ import { z } from "zod";
12
15
 
13
16
  @injectable()
14
17
  export class NodeBackedToolRuntime {
15
18
  constructor(
16
19
  @inject(CoreTokens.NodeResolver)
17
20
  private readonly nodeResolver: NodeResolver,
21
+ @inject(ItemValueResolver)
22
+ private readonly itemValueResolver: ItemValueResolver,
23
+ @inject(NodeOutputNormalizer)
24
+ private readonly outputNormalizer: NodeOutputNormalizer,
18
25
  ) {}
19
26
 
20
27
  async execute(
@@ -32,7 +39,7 @@ export class NodeBackedToolRuntime {
32
39
  const nodeCtx = {
33
40
  ...args.ctx,
34
41
  config: config.node,
35
- } as NodeExecutionContext<any>;
42
+ } as NodeExecutionContext<RunnableNodeConfig>;
36
43
  const resolvedNode = this.nodeResolver.resolve(config.node.type);
37
44
  const outputs = await this.executeResolvedNode(resolvedNode, nodeInput, nodeCtx);
38
45
  return config.toToolOutput({
@@ -49,19 +56,44 @@ export class NodeBackedToolRuntime {
49
56
  private async executeResolvedNode(
50
57
  resolvedNode: unknown,
51
58
  nodeInput: ToolExecuteArgs["item"],
52
- ctx: NodeExecutionContext<any>,
59
+ ctx: NodeExecutionContext<RunnableNodeConfig>,
53
60
  ): Promise<NodeOutputs> {
54
61
  if (this.isMultiInputNode(resolvedNode)) {
55
62
  return await resolvedNode.executeMulti({ in: [nodeInput] }, ctx);
56
63
  }
57
- if (this.isNode(resolvedNode)) {
58
- return await resolvedNode.execute([nodeInput], ctx);
64
+ if (this.isRunnableNode(resolvedNode)) {
65
+ const runnable = resolvedNode;
66
+ const runnableConfig = ctx.config;
67
+ const carry = runnableConfig.lineageCarry ?? "emitOnly";
68
+ const inputSchema = runnable.inputSchema ?? runnableConfig.inputSchema ?? z.unknown();
69
+ const parsed = inputSchema.parse(nodeInput.json);
70
+ const items = [nodeInput];
71
+ const resolvedCtx = await this.itemValueResolver.resolveConfigForItem(ctx, nodeInput, 0, items);
72
+ const execArgs: RunnableNodeExecuteArgs = {
73
+ input: parsed,
74
+ item: nodeInput,
75
+ itemIndex: 0,
76
+ items,
77
+ ctx: resolvedCtx,
78
+ };
79
+ const raw = await Promise.resolve(runnable.execute(execArgs));
80
+ return this.outputNormalizer.normalizeExecuteResult({
81
+ baseItem: nodeInput,
82
+ raw,
83
+ carry,
84
+ });
59
85
  }
60
86
  throw new Error(`Node-backed tool expected a runnable node instance for "${ctx.config.name ?? ctx.nodeId}".`);
61
87
  }
62
88
 
63
- private isNode(value: unknown): value is Node<any> {
64
- return typeof value === "object" && value !== null && "execute" in value;
89
+ private isRunnableNode(value: unknown): value is RunnableNode {
90
+ return (
91
+ typeof value === "object" &&
92
+ value !== null &&
93
+ (value as { kind?: string }).kind === "node" &&
94
+ typeof (value as { execute?: unknown }).execute === "function" &&
95
+ typeof (value as { executeMulti?: unknown }).executeMulti !== "function"
96
+ );
65
97
  }
66
98
 
67
99
  private isMultiInputNode(value: unknown): value is MultiInputNode<any> {
@@ -1,26 +1,16 @@
1
- import type { Item, Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
1
+ import type { Item, RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
2
 
3
3
  import { node } from "@codemation/core";
4
4
 
5
5
  import type { Split } from "./split";
6
6
 
7
7
  @node({ packageName: "@codemation/core-nodes" })
8
- export class SplitNode implements Node<Split<any, any>> {
8
+ export class SplitNode implements RunnableNode<Split<any, any>> {
9
9
  kind = "node" as const;
10
10
  outputPorts = ["main"] as const;
11
11
 
12
- async execute(items: Items, ctx: NodeExecutionContext<Split<any, any>>): Promise<NodeOutputs> {
13
- const out: Item[] = [];
14
- for (let i = 0; i < items.length; i++) {
15
- const item = items[i] as Item;
16
- const elements = ctx.config.getElements(item, ctx);
17
- for (let j = 0; j < elements.length; j++) {
18
- out.push({
19
- ...item,
20
- json: elements[j],
21
- });
22
- }
23
- }
24
- return { main: out };
12
+ execute(args: RunnableNodeExecuteArgs<Split<any, any>>): unknown {
13
+ const elements = args.ctx.config.getElements(args.item as Item, args.ctx);
14
+ return elements;
25
15
  }
26
16
  }