@codemation/core-nodes 0.0.22 → 0.0.25

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.22",
3
+ "version": "0.0.25",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,7 +31,7 @@
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.1"
34
+ "@codemation/core": "0.3.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.3.5",
package/src/index.ts CHANGED
@@ -6,7 +6,10 @@ export * from "./chatModels/OpenAiChatModelPresetsFactory";
6
6
  export * from "./nodes/aiAgent";
7
7
  export * from "./nodes/CallbackNodeFactory";
8
8
  export * from "./nodes/httpRequest";
9
+ export * from "./nodes/aggregate";
10
+ export * from "./nodes/filter";
9
11
  export * from "./nodes/if";
12
+ export * from "./nodes/split";
10
13
  export * from "./nodes/ManualTriggerFactory";
11
14
  export * from "./nodes/mapData";
12
15
  export * from "./nodes/merge";
@@ -4,15 +4,17 @@ import {
4
4
  type AgentMessageConfig,
5
5
  type AgentNodeConfig,
6
6
  type ChatModelConfig,
7
+ type ItemInputMapper,
7
8
  type RetryPolicySpec,
8
9
  type RunnableNodeConfig,
9
10
  type ToolConfig,
10
11
  type TypeToken,
11
12
  } from "@codemation/core";
13
+ import type { ZodType } from "zod";
12
14
 
13
15
  import { AIAgentNode } from "./AIAgentNode";
14
16
 
15
- export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown> {
17
+ export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown, TWireJson = TInputJson> {
16
18
  readonly name: string;
17
19
  readonly messages: AgentMessageConfig<TInputJson>;
18
20
  readonly chatModel: ChatModelConfig;
@@ -20,14 +22,18 @@ export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown> {
20
22
  readonly id?: string;
21
23
  readonly retryPolicy?: RetryPolicySpec;
22
24
  readonly guardrails?: AgentGuardrailConfig;
25
+ /** Engine applies with {@link RunnableNodeConfig.inputSchema} before {@link AIAgentNode.executeOne}. */
26
+ 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>;
23
29
  }
24
30
 
25
31
  /**
26
32
  * AI agent: credential bindings are keyed to connection-owned LLM/tool node ids (ConnectionNodeIdFactory),
27
33
  * not to the agent workflow node id.
28
34
  */
29
- export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
30
- implements RunnableNodeConfig<TInputJson, TOutputJson>, AgentNodeConfig<TInputJson, TOutputJson>
35
+ export class AIAgent<TInputJson = unknown, TOutputJson = unknown, TWireJson = TInputJson>
36
+ implements RunnableNodeConfig<TInputJson, TOutputJson, TWireJson>, AgentNodeConfig<TInputJson, TOutputJson, TWireJson>
31
37
  {
32
38
  readonly kind = "node" as const;
33
39
  readonly type: TypeToken<unknown> = AIAgentNode;
@@ -40,8 +46,10 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
40
46
  readonly id?: string;
41
47
  readonly retryPolicy: RetryPolicySpec;
42
48
  readonly guardrails?: AgentGuardrailConfig;
49
+ readonly inputSchema?: ZodType<TInputJson>;
50
+ readonly mapInput?: ItemInputMapper<TWireJson, TInputJson>;
43
51
 
44
- constructor(options: AIAgentOptions<TInputJson, TOutputJson>) {
52
+ constructor(options: AIAgentOptions<TInputJson, TOutputJson, TWireJson>) {
45
53
  this.name = options.name;
46
54
  this.messages = options.messages;
47
55
  this.chatModel = options.chatModel;
@@ -49,5 +57,7 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
49
57
  this.id = options.id;
50
58
  this.retryPolicy = options.retryPolicy ?? RetryPolicy.defaultForAiAgent;
51
59
  this.guardrails = options.guardrails;
60
+ this.inputSchema = options.inputSchema;
61
+ this.mapInput = options.mapInput;
52
62
  }
53
63
  }
@@ -4,13 +4,12 @@ import type {
4
4
  ChatModelConfig,
5
5
  ChatModelFactory,
6
6
  Item,
7
+ ItemNode,
7
8
  Items,
8
9
  JsonValue,
9
10
  LangChainChatModelLike,
10
- Node,
11
11
  NodeExecutionContext,
12
12
  NodeInputsByPort,
13
- NodeOutputs,
14
13
  Tool,
15
14
  ToolConfig,
16
15
  ZodSchemaAny,
@@ -30,6 +29,7 @@ import {
30
29
  } from "@codemation/core";
31
30
 
32
31
  import { AIMessage, type BaseMessage } from "@langchain/core/messages";
32
+ import { z } from "zod";
33
33
 
34
34
  import type { AIAgent } from "./AIAgentConfig";
35
35
  import { AIAgentExecutionHelpersFactory } from "./AIAgentExecutionHelpersFactory";
@@ -49,7 +49,7 @@ import {
49
49
  type ResolvedGuardrails = Required<Pick<AgentGuardrailConfig, "maxTurns" | "onTurnLimitReached">> &
50
50
  Pick<AgentGuardrailConfig, "modelInvocationOptions">;
51
51
 
52
- /** Everything needed to run the agent loop for a workflow execution (one `execute` call). */
52
+ /** Everything needed to run the agent loop for one item (shared across items in the same activation). */
53
53
  interface PreparedAgentExecution {
54
54
  readonly ctx: NodeExecutionContext<AIAgent<any, any>>;
55
55
  readonly model: LangChainChatModelLike;
@@ -59,11 +59,21 @@ interface PreparedAgentExecution {
59
59
  }
60
60
 
61
61
  @node({ packageName: "@codemation/core-nodes" })
62
- export class AIAgentNode implements Node<AIAgent<any, any>> {
62
+ export class AIAgentNode implements ItemNode<AIAgent<any, any>, unknown, unknown> {
63
63
  kind = "node" as const;
64
64
  outputPorts = ["main"] as const;
65
+ /**
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.
68
+ */
69
+ readonly inputSchema = z.unknown();
65
70
 
66
71
  private readonly connectionCredentialExecutionContextFactory: ConnectionCredentialExecutionContextFactory;
72
+ /** One resolved model/tools bundle per activation context (same ctx across items in a batch). */
73
+ private readonly preparedByExecutionContext = new WeakMap<
74
+ NodeExecutionContext<AIAgent<any, any>>,
75
+ Promise<PreparedAgentExecution>
76
+ >();
67
77
 
68
78
  constructor(
69
79
  @inject(CoreTokens.NodeResolver)
@@ -79,17 +89,35 @@ export class AIAgentNode implements Node<AIAgent<any, any>> {
79
89
  this.executionHelpers.createConnectionCredentialExecutionContextFactory(credentialSessions);
80
90
  }
81
91
 
82
- async execute(items: Items, ctx: NodeExecutionContext<AIAgent<any, any>>): Promise<NodeOutputs> {
83
- const prepared = await this.prepareExecution(ctx);
84
- const out: Item[] = [];
85
- for (let i = 0; i < items.length; i++) {
86
- out.push(await this.runAgentForItem(prepared, items[i]!, i, items));
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> {
99
+ const prepared = await this.getOrPrepareExecution(args.ctx);
100
+ const itemWithMappedJson = { ...args.item, json: args.input };
101
+ const resultItem = await this.runAgentForItem(prepared, itemWithMappedJson, args.itemIndex, args.items);
102
+ return resultItem.json;
103
+ }
104
+
105
+ private async getOrPrepareExecution(ctx: NodeExecutionContext<AIAgent<any, any>>): Promise<PreparedAgentExecution> {
106
+ let pending = this.preparedByExecutionContext.get(ctx);
107
+ if (!pending) {
108
+ pending = this.prepareExecution(ctx);
109
+ this.preparedByExecutionContext.set(ctx, pending);
110
+ }
111
+ try {
112
+ return await pending;
113
+ } catch (error) {
114
+ this.preparedByExecutionContext.delete(ctx);
115
+ throw error;
87
116
  }
88
- return { main: out };
89
117
  }
90
118
 
91
119
  /**
92
- * Resolves the chat model and tools once, then returns shared state for every item in the batch.
120
+ * Resolves the chat model and tools once per activation, then reuses for every item in the batch.
93
121
  */
94
122
  private async prepareExecution(ctx: NodeExecutionContext<AIAgent<any, any>>): Promise<PreparedAgentExecution> {
95
123
  const chatModelFactory = this.nodeResolver.resolve(ctx.config.chatModel.type) as ChatModelFactory<ChatModelConfig>;
@@ -467,7 +495,7 @@ export class AIAgentNode implements Node<AIAgent<any, any>> {
467
495
  ctx: NodeExecutionContext<AIAgent<any, any>>,
468
496
  ): ReadonlyArray<BaseMessage> {
469
497
  return AgentMessageFactory.createPromptMessages(
470
- AgentMessageConfigNormalizer.normalize(ctx.config, {
498
+ AgentMessageConfigNormalizer.resolveFromInputOrConfig(item.json, ctx.config, {
471
499
  item,
472
500
  itemIndex,
473
501
  items,
@@ -0,0 +1,19 @@
1
+ import type { Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
2
+
3
+ import { node } from "@codemation/core";
4
+
5
+ import type { Aggregate } from "./aggregate";
6
+
7
+ @node({ packageName: "@codemation/core-nodes" })
8
+ export class AggregateNode implements Node<Aggregate<any, any>> {
9
+ kind = "node" as const;
10
+ outputPorts = ["main"] as const;
11
+
12
+ async execute(items: Items, ctx: NodeExecutionContext<Aggregate<any, any>>): Promise<NodeOutputs> {
13
+ if (items.length === 0) {
14
+ return { main: [] };
15
+ }
16
+ const json = await Promise.resolve(ctx.config.aggregate(items as Items, ctx));
17
+ return { main: [{ json }] };
18
+ }
19
+ }
@@ -0,0 +1,22 @@
1
+ import type { Item, Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
2
+
3
+ import { node } from "@codemation/core";
4
+
5
+ import type { Filter } from "./filter";
6
+
7
+ @node({ packageName: "@codemation/core-nodes" })
8
+ export class FilterNode implements Node<Filter<any>> {
9
+ kind = "node" as const;
10
+ outputPorts = ["main"] as const;
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
+ }
19
+ }
20
+ return { main: out };
21
+ }
22
+ }
@@ -1,20 +1,23 @@
1
- import type { Item, Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
1
+ import type { Item, ItemNode, Items, NodeExecutionContext } from "@codemation/core";
2
2
 
3
3
  import { node } from "@codemation/core";
4
+ import { z } from "zod";
4
5
 
5
6
  import { MapData } from "./mapData";
6
7
 
7
8
  @node({ packageName: "@codemation/core-nodes" })
8
- export class MapDataNode implements Node<MapData<any, any>> {
9
+ export class MapDataNode implements ItemNode<MapData<any, any>, unknown, unknown> {
9
10
  kind = "node" as const;
10
11
  outputPorts = ["main"] as const;
12
+ readonly inputSchema = z.unknown();
11
13
 
12
- async execute(items: Items, ctx: NodeExecutionContext<MapData<any, any>>): Promise<NodeOutputs> {
13
- const out: Item[] = [];
14
- for (let i = 0; i < items.length; i++) {
15
- const item = items[i] as Item<unknown>;
16
- out.push({ ...item, json: ctx.config.map(item, ctx) });
17
- }
18
- return { main: out };
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> {
21
+ return args.ctx.config.map(args.item, args.ctx);
19
22
  }
20
23
  }
@@ -0,0 +1,26 @@
1
+ import type { Item, Items, Node, NodeExecutionContext, NodeOutputs } from "@codemation/core";
2
+
3
+ import { node } from "@codemation/core";
4
+
5
+ import type { Split } from "./split";
6
+
7
+ @node({ packageName: "@codemation/core-nodes" })
8
+ export class SplitNode implements Node<Split<any, any>> {
9
+ kind = "node" as const;
10
+ outputPorts = ["main"] as const;
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 };
25
+ }
26
+ }
@@ -0,0 +1,21 @@
1
+ import type { Items, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
+
3
+ import { AggregateNode } from "./AggregateNode";
4
+
5
+ export class Aggregate<TIn = unknown, TOut = unknown> implements RunnableNodeConfig<TIn, TOut> {
6
+ readonly kind = "node" as const;
7
+ readonly type: TypeToken<unknown> = AggregateNode;
8
+ readonly execution = { hint: "local" } as const;
9
+ readonly icon = "lucide:layers" as const;
10
+
11
+ constructor(
12
+ public readonly name: string,
13
+ public readonly aggregate: (
14
+ items: Items<TIn>,
15
+ ctx: NodeExecutionContext<Aggregate<TIn, TOut>>,
16
+ ) => TOut | Promise<TOut>,
17
+ public readonly id?: string,
18
+ ) {}
19
+ }
20
+
21
+ export { AggregateNode } from "./AggregateNode";
@@ -0,0 +1,23 @@
1
+ import type { Item, Items, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
+
3
+ import { FilterNode } from "./FilterNode";
4
+
5
+ export class Filter<TIn = unknown> implements RunnableNodeConfig<TIn, TIn> {
6
+ readonly kind = "node" as const;
7
+ readonly type: TypeToken<unknown> = FilterNode;
8
+ readonly execution = { hint: "local" } as const;
9
+ readonly icon = "lucide:filter" as const;
10
+
11
+ constructor(
12
+ public readonly name: string,
13
+ public readonly predicate: (
14
+ item: Item<TIn>,
15
+ index: number,
16
+ items: Items<TIn>,
17
+ ctx: NodeExecutionContext<Filter<TIn>>,
18
+ ) => boolean,
19
+ public readonly id?: string,
20
+ ) {}
21
+ }
22
+
23
+ export { FilterNode } from "./FilterNode";
@@ -0,0 +1,23 @@
1
+ import type { Item, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
+
3
+ import { SplitNode } from "./SplitNode";
4
+
5
+ export class Split<TIn = unknown, TElem = unknown> implements RunnableNodeConfig<TIn, TElem> {
6
+ readonly kind = "node" as const;
7
+ readonly type: TypeToken<unknown> = SplitNode;
8
+ readonly execution = { hint: "local" } as const;
9
+ /**
10
+ * When splitting yields zero items for a batch, downstream single-input nodes still run once with an empty batch.
11
+ * Mirrors {@link MapData}'s empty-output behavior.
12
+ */
13
+ readonly continueWhenEmptyOutput = true as const;
14
+ readonly icon = "lucide:ungroup" as const;
15
+
16
+ constructor(
17
+ public readonly name: string,
18
+ public readonly getElements: (item: Item<TIn>, ctx: NodeExecutionContext<Split<TIn, TElem>>) => readonly TElem[],
19
+ public readonly id?: string,
20
+ ) {}
21
+ }
22
+
23
+ export { SplitNode } from "./SplitNode";
@@ -9,6 +9,9 @@ import { NoOpNode } from "./nodes/noOp";
9
9
  import { SubWorkflowNode } from "./nodes/subWorkflow";
10
10
  import { WaitNode } from "./nodes/wait";
11
11
  import { ConnectionCredentialNode } from "./nodes/ConnectionCredentialNode";
12
+ import { AggregateNode } from "./nodes/aggregate";
13
+ import { FilterNode } from "./nodes/filter";
14
+ import { SplitNode } from "./nodes/split";
12
15
 
13
16
  /**
14
17
  * Registrar for built-in nodes. In a real project, this would use tsyringe's
@@ -34,4 +37,7 @@ export function registerCoreNodes(container: Container): void {
34
37
  void AIAgentExecutionHelpersFactory;
35
38
  void WaitNode;
36
39
  void ConnectionCredentialNode;
40
+ void SplitNode;
41
+ void FilterNode;
42
+ void AggregateNode;
37
43
  }
@@ -10,7 +10,7 @@ import { WorkflowDurationParser } from "./WorkflowDurationParser.types";
10
10
  export class WorkflowBranchBuilder<TCurrentJson> {
11
11
  constructor(private readonly steps: ReadonlyArray<AnyRunnableNodeConfig> = []) {}
12
12
 
13
- then<TConfig extends RunnableNodeConfig<TCurrentJson, any>>(
13
+ then<TInputJson, TOutputJson, TConfig extends RunnableNodeConfig<TInputJson, TOutputJson, TCurrentJson>>(
14
14
  config: TConfig,
15
15
  ): WorkflowBranchBuilder<RunnableNodeOutputJson<TConfig>> {
16
16
  return new WorkflowBranchBuilder<RunnableNodeOutputJson<TConfig>>([...this.steps, config]);
@@ -18,7 +18,7 @@ type BranchOutputMatch<TLeft, TRight> = [TLeft] extends [TRight] ? ([TRight] ext
18
18
  export class WorkflowChain<TCurrentJson> {
19
19
  constructor(private readonly chain: ChainCursor<TCurrentJson>) {}
20
20
 
21
- then<TConfig extends RunnableNodeConfig<TCurrentJson, any>>(
21
+ then<TInputJson, TOutputJson, TConfig extends RunnableNodeConfig<TInputJson, TOutputJson, TCurrentJson>>(
22
22
  config: TConfig,
23
23
  ): WorkflowChain<RunnableNodeOutputJson<TConfig>> {
24
24
  return new WorkflowChain(this.chain.then(config));