@codemation/core-nodes 0.0.18 → 0.0.21

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.18",
3
+ "version": "0.0.21",
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.0.18"
34
+ "@codemation/core": "0.2.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.3.5",
@@ -1,7 +1,10 @@
1
- import type { CredentialSessionService, Item, Items, NodeExecutionContext } from "@codemation/core";
1
+ import type { CredentialSessionService, Item, Items, NodeExecutionContext, ZodSchemaAny } from "@codemation/core";
2
2
  import { injectable } from "@codemation/core";
3
3
 
4
+ import { isInteropZodSchema } from "@langchain/core/utils/types";
5
+ import { toJsonSchema } from "@langchain/core/utils/json_schema";
4
6
  import { DynamicStructuredTool } from "@langchain/core/tools";
7
+ import { toJSONSchema } from "zod/v4/core";
5
8
 
6
9
  import { ConnectionCredentialExecutionContextFactory } from "./ConnectionCredentialExecutionContextFactory";
7
10
  import type { ResolvedTool } from "./aiAgentSupport.types";
@@ -25,10 +28,19 @@ export class AIAgentExecutionHelpersFactory {
25
28
  itemIndex: number,
26
29
  items: Items,
27
30
  ): DynamicStructuredTool {
31
+ if (entry.runtime.inputSchema == null) {
32
+ throw new Error(
33
+ `Cannot create LangChain tool "${entry.config.name}": missing inputSchema (broken tool runtime resolution).`,
34
+ );
35
+ }
36
+ const schemaForOpenAi = this.normalizeToolInputSchemaForOpenAiDynamicStructuredTool(
37
+ entry.config.name,
38
+ entry.runtime.inputSchema,
39
+ );
28
40
  return new DynamicStructuredTool({
29
41
  name: entry.config.name,
30
42
  description: entry.config.description ?? entry.runtime.defaultDescription,
31
- schema: entry.runtime.inputSchema,
43
+ schema: schemaForOpenAi as unknown as ZodSchemaAny,
32
44
  func: async (input) => {
33
45
  const result = await entry.runtime.execute({
34
46
  config: entry.config,
@@ -42,4 +54,90 @@ export class AIAgentExecutionHelpersFactory {
42
54
  },
43
55
  });
44
56
  }
57
+
58
+ /**
59
+ * Produces a plain JSON Schema object for OpenAI tool parameters and LangChain tool invocation:
60
+ * - **Zod** → `toJSONSchema(..., { target: "draft-07" })` so shapes match what `@cfworker/json-schema`
61
+ * expects (`required` must be an array; draft 2020-12 output can break validation).
62
+ * - Otherwise LangChain `toJsonSchema` (Standard Schema + JSON passthrough); if the result is still Zod
63
+ * (duplicate `zod` copies), fall back to Zod `toJSONSchema` with draft-07.
64
+ * - Strip root `$schema` for OpenAI; normalize invalid `required` keywords for cfworker; ensure `properties`.
65
+ */
66
+ private normalizeToolInputSchemaForOpenAiDynamicStructuredTool(
67
+ toolName: string,
68
+ inputSchema: ZodSchemaAny,
69
+ ): Record<string, unknown> {
70
+ const draft07Params = { target: "draft-07" as const };
71
+ let converted: unknown;
72
+ if (isInteropZodSchema(inputSchema)) {
73
+ converted = toJSONSchema(inputSchema as unknown as Parameters<typeof toJSONSchema>[0], draft07Params);
74
+ } else {
75
+ converted = toJsonSchema(inputSchema);
76
+ if (isInteropZodSchema(converted)) {
77
+ converted = toJSONSchema(inputSchema as unknown as Parameters<typeof toJSONSchema>[0], draft07Params);
78
+ }
79
+ }
80
+ const record = converted as Record<string, unknown>;
81
+ const { $schema: _draftSchemaOmitted, ...rest } = record;
82
+ if (rest.type !== "object") {
83
+ throw new Error(
84
+ `Cannot create LangChain tool "${toolName}": tool input schema must be a JSON Schema object type (got type=${String(rest.type)}).`,
85
+ );
86
+ }
87
+ if (rest.properties !== undefined && (typeof rest.properties !== "object" || Array.isArray(rest.properties))) {
88
+ throw new Error(
89
+ `Cannot create LangChain tool "${toolName}": tool input schema "properties" must be an object (got ${JSON.stringify(rest.properties)}).`,
90
+ );
91
+ }
92
+ if (rest.properties === undefined) {
93
+ rest.properties = {};
94
+ }
95
+ this.sanitizeJsonSchemaRequiredKeywordsForCfworker(rest);
96
+ return rest;
97
+ }
98
+
99
+ /**
100
+ * `@cfworker/json-schema` iterates `schema.required` with `for...of`; it must be a string array or absent.
101
+ */
102
+ private sanitizeJsonSchemaRequiredKeywordsForCfworker(node: unknown): void {
103
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
104
+ return;
105
+ }
106
+ const o = node as Record<string, unknown>;
107
+ const req = o.required;
108
+ if (req !== undefined && !Array.isArray(req)) {
109
+ delete o.required;
110
+ } else if (Array.isArray(req)) {
111
+ const strings = req.filter((x): x is string => typeof x === "string");
112
+ if (strings.length === 0) {
113
+ delete o.required;
114
+ } else if (strings.length !== req.length) {
115
+ o.required = strings;
116
+ }
117
+ }
118
+ const props = o.properties;
119
+ if (props && typeof props === "object" && !Array.isArray(props)) {
120
+ for (const v of Object.values(props)) {
121
+ this.sanitizeJsonSchemaRequiredKeywordsForCfworker(v);
122
+ }
123
+ }
124
+ for (const key of ["allOf", "anyOf", "oneOf"] as const) {
125
+ const branch = o[key];
126
+ if (Array.isArray(branch)) {
127
+ for (const sub of branch) {
128
+ this.sanitizeJsonSchemaRequiredKeywordsForCfworker(sub);
129
+ }
130
+ }
131
+ }
132
+ if (o.if) this.sanitizeJsonSchemaRequiredKeywordsForCfworker(o.if);
133
+ if (o.then) this.sanitizeJsonSchemaRequiredKeywordsForCfworker(o.then);
134
+ if (o.else) this.sanitizeJsonSchemaRequiredKeywordsForCfworker(o.else);
135
+ if (o.not) this.sanitizeJsonSchemaRequiredKeywordsForCfworker(o.not);
136
+ if (o.items) this.sanitizeJsonSchemaRequiredKeywordsForCfworker(o.items);
137
+ if (Array.isArray(o.prefixItems)) {
138
+ for (const sub of o.prefixItems) {
139
+ this.sanitizeJsonSchemaRequiredKeywordsForCfworker(sub);
140
+ }
141
+ }
142
+ }
45
143
  }
@@ -477,14 +477,23 @@ export class AIAgentNode implements Node<AIAgent<any, any>> {
477
477
  }
478
478
 
479
479
  private resolveToolRuntime(config: ToolConfig): ResolvedTool["runtime"] {
480
- if (config instanceof NodeBackedToolConfig) {
480
+ if (this.isNodeBackedToolConfig(config)) {
481
+ const inputSchema = config.getInputSchema();
482
+ if (inputSchema == null) {
483
+ throw new Error(
484
+ `AIAgent tool "${config.name}": node-backed tool is missing inputSchema (cannot build LangChain tool).`,
485
+ );
486
+ }
481
487
  return {
482
488
  defaultDescription: `Run workflow node "${config.node.name ?? config.name}" as an AI tool.`,
483
- inputSchema: config.getInputSchema(),
489
+ inputSchema,
484
490
  execute: async (args) => await this.nodeBackedToolRuntime.execute(config, args),
485
491
  };
486
492
  }
487
493
  const tool = this.nodeResolver.resolve(config.type) as Tool<ToolConfig, ZodSchemaAny, ZodSchemaAny>;
494
+ if (tool.inputSchema == null) {
495
+ throw new Error(`AIAgent tool "${config.name}": plugin tool "${String(config.type)}" is missing inputSchema.`);
496
+ }
488
497
  return {
489
498
  defaultDescription: tool.defaultDescription,
490
499
  inputSchema: tool.inputSchema,
@@ -492,6 +501,18 @@ export class AIAgentNode implements Node<AIAgent<any, any>> {
492
501
  };
493
502
  }
494
503
 
504
+ /**
505
+ * Consumer apps can resolve two copies of `@codemation/core`, breaking `instanceof NodeBackedToolConfig` and
506
+ * sending node-backed tools down the plugin-tool branch with `inputSchema: undefined` (LangChain then crashes in
507
+ * json-schema validation). {@link NodeBackedToolConfig#toolKind} is stable across copies.
508
+ */
509
+ private isNodeBackedToolConfig(config: ToolConfig): config is NodeBackedToolConfig<any, any, any> {
510
+ return (
511
+ config instanceof NodeBackedToolConfig ||
512
+ (typeof config === "object" && config !== null && (config as { toolKind?: unknown }).toolKind === "nodeBacked")
513
+ );
514
+ }
515
+
495
516
  private resolveGuardrails(guardrails: AgentGuardrailConfig | undefined): ResolvedGuardrails {
496
517
  const maxTurns = guardrails?.maxTurns ?? AgentGuardrailDefaults.maxTurns;
497
518
  if (!Number.isInteger(maxTurns) || maxTurns < 1) {
@@ -1,5 +1,5 @@
1
1
  import type { NodeDefinition, WorkflowDefinition, WorkflowNodeConnection } from "@codemation/core";
2
- import { AgentConfigInspector, ConnectionNodeIdFactory } from "@codemation/core";
2
+ import { AgentConfigInspector, AgentConnectionNodeCollector } from "@codemation/core";
3
3
 
4
4
  import { AIAgentNode } from "../nodes/AIAgentNode";
5
5
  import { ConnectionCredentialNode } from "../nodes/ConnectionCredentialNode";
@@ -12,65 +12,95 @@ export class AIAgentConnectionWorkflowExpander {
12
12
  constructor(private readonly connectionCredentialNodeConfigFactory: ConnectionCredentialNodeConfigFactory) {}
13
13
 
14
14
  expand(workflow: WorkflowDefinition): WorkflowDefinition {
15
- const existingByParentAndName = new Map<string, WorkflowNodeConnection>();
16
- for (const c of workflow.connections ?? []) {
17
- existingByParentAndName.set(`${c.parentNodeId}\0${c.connectionName}`, c);
18
- }
19
-
15
+ const existingChildIds = this.collectExistingChildIds(workflow);
16
+ const connectionsByParentAndName = this.createConnectionsByParentAndName(workflow);
20
17
  const extraNodes: NodeDefinition[] = [];
21
- const extraConnections: WorkflowNodeConnection[] = [];
18
+ let connectionsChanged = false;
22
19
 
23
20
  for (const node of workflow.nodes) {
24
21
  if (node.type !== AIAgentNode || !AgentConfigInspector.isAgentNodeConfig(node.config)) {
25
22
  continue;
26
23
  }
27
- const agentId = node.id;
28
- const agentConfig = node.config;
29
-
30
- if (!existingByParentAndName.has(`${agentId}\0llm`)) {
31
- const llmId = ConnectionNodeIdFactory.languageModelConnectionNodeId(agentId);
32
- this.assertNoIdCollision(workflow, extraNodes, llmId);
33
- extraNodes.push({
34
- id: llmId,
35
- kind: "node",
36
- type: ConnectionCredentialNode,
37
- name: agentConfig.chatModel.presentation?.label ?? agentConfig.chatModel.name,
38
- config: this.connectionCredentialNodeConfigFactory.create(agentConfig.chatModel.name, agentConfig.chatModel),
39
- });
40
- extraConnections.push({ parentNodeId: agentId, connectionName: "llm", childNodeIds: [llmId] });
41
- }
42
-
43
- if (!existingByParentAndName.has(`${agentId}\0tools`) && (agentConfig.tools?.length ?? 0) > 0) {
44
- const toolIds: string[] = [];
45
- for (const tool of agentConfig.tools ?? []) {
46
- const toolId = ConnectionNodeIdFactory.toolConnectionNodeId(agentId, tool.name);
47
- this.assertNoIdCollision(workflow, extraNodes, toolId);
48
- toolIds.push(toolId);
24
+ for (const connectionNode of AgentConnectionNodeCollector.collect(node.id, node.config)) {
25
+ if (!existingChildIds.has(connectionNode.nodeId)) {
26
+ this.assertNoIdCollision(workflow, extraNodes, existingChildIds, connectionNode.nodeId);
49
27
  extraNodes.push({
50
- id: toolId,
28
+ id: connectionNode.nodeId,
51
29
  kind: "node",
52
30
  type: ConnectionCredentialNode,
53
- name: tool.presentation?.label ?? tool.name,
54
- config: this.connectionCredentialNodeConfigFactory.create(tool.name, tool),
31
+ name: connectionNode.name,
32
+ config: this.connectionCredentialNodeConfigFactory.create(
33
+ connectionNode.typeName,
34
+ connectionNode.credentialSource,
35
+ ),
55
36
  });
56
37
  }
57
- extraConnections.push({ parentNodeId: agentId, connectionName: "tools", childNodeIds: toolIds });
38
+ const connectionKey = this.connectionKey(connectionNode.parentNodeId, connectionNode.connectionName);
39
+ const existingConnection = connectionsByParentAndName.get(connectionKey);
40
+ if (!existingConnection) {
41
+ connectionsByParentAndName.set(connectionKey, {
42
+ parentNodeId: connectionNode.parentNodeId,
43
+ connectionName: connectionNode.connectionName,
44
+ childNodeIds: [connectionNode.nodeId],
45
+ });
46
+ connectionsChanged = true;
47
+ continue;
48
+ }
49
+ if (!existingConnection.childNodeIds.includes(connectionNode.nodeId)) {
50
+ connectionsByParentAndName.set(connectionKey, {
51
+ ...existingConnection,
52
+ childNodeIds: [...existingConnection.childNodeIds, connectionNode.nodeId],
53
+ });
54
+ connectionsChanged = true;
55
+ }
58
56
  }
59
57
  }
60
58
 
61
- if (extraNodes.length === 0) {
59
+ if (extraNodes.length === 0 && !connectionsChanged) {
62
60
  return workflow;
63
61
  }
64
62
 
65
63
  return {
66
64
  ...workflow,
67
65
  nodes: [...workflow.nodes, ...extraNodes],
68
- connections: [...(workflow.connections ?? []), ...extraConnections],
66
+ connections: [...connectionsByParentAndName.values()],
69
67
  };
70
68
  }
71
69
 
72
- private assertNoIdCollision(workflow: WorkflowDefinition, pending: ReadonlyArray<NodeDefinition>, id: string): void {
73
- if (workflow.nodes.some((n) => n.id === id) || pending.some((n) => n.id === id)) {
70
+ private createConnectionsByParentAndName(workflow: WorkflowDefinition): Map<string, WorkflowNodeConnection> {
71
+ const existingByParentAndName = new Map<string, WorkflowNodeConnection>();
72
+ for (const connection of workflow.connections ?? []) {
73
+ existingByParentAndName.set(this.connectionKey(connection.parentNodeId, connection.connectionName), connection);
74
+ }
75
+ return existingByParentAndName;
76
+ }
77
+
78
+ private collectExistingChildIds(workflow: WorkflowDefinition): ReadonlySet<string> {
79
+ const ids = new Set<string>();
80
+ for (const connection of workflow.connections ?? []) {
81
+ for (const childId of connection.childNodeIds) {
82
+ ids.add(childId);
83
+ }
84
+ }
85
+ return ids;
86
+ }
87
+
88
+ private connectionKey(parentNodeId: string, connectionName: string): string {
89
+ return `${parentNodeId}\0${connectionName}`;
90
+ }
91
+
92
+ private assertNoIdCollision(
93
+ workflow: WorkflowDefinition,
94
+ pending: ReadonlyArray<NodeDefinition>,
95
+ existingChildIds: ReadonlySet<string>,
96
+ id: string,
97
+ ): void {
98
+ if (pending.some((n) => n.id === id)) {
99
+ throw new Error(
100
+ `AIAgent connection expansion: node id "${id}" already exists. Rename the conflicting node or adjust the workflow.`,
101
+ );
102
+ }
103
+ if (workflow.nodes.some((n) => n.id === id) && !existingChildIds.has(id)) {
74
104
  throw new Error(
75
105
  `AIAgent connection expansion: node id "${id}" already exists. Rename the conflicting node or adjust the workflow.`,
76
106
  );