@codemation/core-nodes 0.0.19 → 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/CHANGELOG.md +16 -0
- package/dist/index.cjs +938 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +937 -46
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +100 -2
- package/src/nodes/AIAgentNode.ts +23 -2
- package/src/workflows/AIAgentConnectionWorkflowExpander.ts +67 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/core-nodes",
|
|
3
|
-
"version": "0.0.
|
|
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
|
|
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:
|
|
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
|
}
|
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
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:
|
|
28
|
+
id: connectionNode.nodeId,
|
|
51
29
|
kind: "node",
|
|
52
30
|
type: ConnectionCredentialNode,
|
|
53
|
-
name:
|
|
54
|
-
config: this.connectionCredentialNodeConfigFactory.create(
|
|
31
|
+
name: connectionNode.name,
|
|
32
|
+
config: this.connectionCredentialNodeConfigFactory.create(
|
|
33
|
+
connectionNode.typeName,
|
|
34
|
+
connectionNode.credentialSource,
|
|
35
|
+
),
|
|
55
36
|
});
|
|
56
37
|
}
|
|
57
|
-
|
|
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: [...(
|
|
66
|
+
connections: [...connectionsByParentAndName.values()],
|
|
69
67
|
};
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
private
|
|
73
|
-
|
|
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
|
);
|