@codemation/core-nodes 0.4.3 → 1.0.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/CHANGELOG.md +44 -0
- package/dist/index.cjs +480 -410
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +220 -116
- package/dist/index.d.ts +220 -116
- package/dist/index.js +476 -403
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
- package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
- package/src/index.ts +1 -1
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
- package/src/nodes/AIAgentNode.ts +293 -288
- package/src/nodes/AgentMessageFactory.ts +57 -49
- package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
- package/src/nodes/AgentToolExecutionCoordinator.ts +3 -14
- package/src/nodes/aiAgentSupport.types.ts +7 -2
- package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/core-nodes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"@
|
|
31
|
+
"@ai-sdk/openai": "^3.0.53",
|
|
32
|
+
"@ai-sdk/provider": "^3.0.8",
|
|
33
|
+
"ai": "^6.0.168",
|
|
33
34
|
"lucide-react": "^0.577.0",
|
|
34
|
-
"@codemation/core": "0.
|
|
35
|
+
"@codemation/core": "1.0.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/node": "^25.3.5",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ChatLanguageModel, ChatModelFactory, NodeExecutionContext } from "@codemation/core";
|
|
2
2
|
import { chatModel } from "@codemation/core";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
5
|
+
|
|
4
6
|
import type { OpenAiCredentialSession } from "./OpenAiCredentialSession";
|
|
5
7
|
import type { OpenAIChatModelConfig } from "./openAiChatModelConfig";
|
|
6
8
|
|
|
@@ -8,14 +10,21 @@ import type { OpenAIChatModelConfig } from "./openAiChatModelConfig";
|
|
|
8
10
|
export class OpenAIChatModelFactory implements ChatModelFactory<OpenAIChatModelConfig> {
|
|
9
11
|
async create(
|
|
10
12
|
args: Readonly<{ config: OpenAIChatModelConfig; ctx: NodeExecutionContext<any> }>,
|
|
11
|
-
): Promise<
|
|
13
|
+
): Promise<ChatLanguageModel> {
|
|
12
14
|
const session = await args.ctx.getCredential<OpenAiCredentialSession>(args.config.credentialSlotKey);
|
|
13
|
-
|
|
15
|
+
const provider = createOpenAI({
|
|
14
16
|
apiKey: session.apiKey,
|
|
15
|
-
|
|
16
|
-
temperature: args.config.options?.temperature,
|
|
17
|
-
maxTokens: args.config.options?.maxTokens,
|
|
18
|
-
configuration: session.baseUrl ? { baseURL: session.baseUrl } : undefined,
|
|
17
|
+
baseURL: session.baseUrl,
|
|
19
18
|
});
|
|
19
|
+
const languageModel = provider.chat(args.config.model);
|
|
20
|
+
return {
|
|
21
|
+
languageModel,
|
|
22
|
+
modelName: args.config.model,
|
|
23
|
+
provider: "openai",
|
|
24
|
+
defaultCallOptions: {
|
|
25
|
+
maxOutputTokens: args.config.options?.maxTokens,
|
|
26
|
+
temperature: args.config.options?.temperature,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
20
29
|
}
|
|
21
30
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ZodSchemaAny } from "@codemation/core";
|
|
2
|
+
import { inject, injectable } from "@codemation/core";
|
|
3
|
+
|
|
4
|
+
import { AIAgentExecutionHelpersFactory } from "../nodes/AIAgentExecutionHelpersFactory";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Produces an OpenAI **strict mode**–compliant JSON Schema for an AIAgent `outputSchema`.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: AI SDK's default Zod → JSON Schema conversion (Zod v4's `toJSONSchema`) can
|
|
10
|
+
* emit `unevaluatedProperties: false` or skip `additionalProperties: false` on object branches.
|
|
11
|
+
* OpenAI's strict-mode validator rejects anything missing `additionalProperties: false` at
|
|
12
|
+
* `context=()` (the root) and requires **all properties** in `required`. We convert here so all
|
|
13
|
+
* legal Zod root shapes work (object, union, discriminated union, nullable-object wrapper, array,
|
|
14
|
+
* intersection, …) and hand AI SDK a pre-tagged `jsonSchema(...)` record that passes straight
|
|
15
|
+
* through to the provider.
|
|
16
|
+
*
|
|
17
|
+
* Rules enforced on the produced JSON Schema record:
|
|
18
|
+
* - Every `type: "object"` node (root and nested under `allOf`/`anyOf`/`oneOf`/`items`/`prefixItems`/`$defs`):
|
|
19
|
+
* - `additionalProperties: false`
|
|
20
|
+
* - `required` lists **every** key in `properties` (OpenAI strict requires all properties required;
|
|
21
|
+
* express optionality via `.nullable()` / `z.union([..., z.null()])`).
|
|
22
|
+
* - `properties` is always an object (empty object allowed).
|
|
23
|
+
* - `$schema`, `unevaluatedProperties`, and `default` are stripped (OpenAI rejects / ignores them).
|
|
24
|
+
* - `sanitizeJsonSchemaRequiredKeywordsForCfworker` invariants from
|
|
25
|
+
* {@link AIAgentExecutionHelpersFactory.createJsonSchemaRecord} are preserved as a starting point.
|
|
26
|
+
*/
|
|
27
|
+
@injectable()
|
|
28
|
+
export class OpenAiStrictJsonSchemaFactory {
|
|
29
|
+
constructor(
|
|
30
|
+
@inject(AIAgentExecutionHelpersFactory)
|
|
31
|
+
private readonly executionHelpers: AIAgentExecutionHelpersFactory,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
createStructuredOutputRecord(
|
|
35
|
+
schema: ZodSchemaAny,
|
|
36
|
+
options: Readonly<{ schemaName: string; title?: string }>,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
const record = this.executionHelpers.createJsonSchemaRecord(schema, {
|
|
39
|
+
schemaName: options.schemaName,
|
|
40
|
+
requireObjectRoot: false,
|
|
41
|
+
});
|
|
42
|
+
this.strictifyRecursive(record);
|
|
43
|
+
if (options.title !== undefined) {
|
|
44
|
+
record.title = options.title;
|
|
45
|
+
}
|
|
46
|
+
return record;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private strictifyRecursive(node: unknown): void {
|
|
50
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const o = node as Record<string, unknown>;
|
|
54
|
+
this.stripOpenAiRejectedKeywords(o);
|
|
55
|
+
if (this.isObjectNode(o)) {
|
|
56
|
+
const props = this.readPropertiesObject(o);
|
|
57
|
+
o.properties = props;
|
|
58
|
+
o.additionalProperties = false;
|
|
59
|
+
o.required = Object.keys(props);
|
|
60
|
+
for (const value of Object.values(props)) {
|
|
61
|
+
this.strictifyRecursive(value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.recurseIntoComposites(o);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private stripOpenAiRejectedKeywords(o: Record<string, unknown>): void {
|
|
68
|
+
delete o["$schema"];
|
|
69
|
+
delete o["unevaluatedProperties"];
|
|
70
|
+
delete o["default"];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private isObjectNode(o: Record<string, unknown>): boolean {
|
|
74
|
+
const typeIsObject =
|
|
75
|
+
o.type === "object" || (Array.isArray(o.type) && (o.type as ReadonlyArray<unknown>).includes("object"));
|
|
76
|
+
const hasObjectProperties =
|
|
77
|
+
o.properties !== undefined && typeof o.properties === "object" && !Array.isArray(o.properties);
|
|
78
|
+
return typeIsObject || hasObjectProperties;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private readPropertiesObject(o: Record<string, unknown>): Record<string, unknown> {
|
|
82
|
+
if (o.properties && typeof o.properties === "object" && !Array.isArray(o.properties)) {
|
|
83
|
+
return o.properties as Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private recurseIntoComposites(o: Record<string, unknown>): void {
|
|
89
|
+
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"] as const) {
|
|
90
|
+
const branch = o[key];
|
|
91
|
+
if (Array.isArray(branch)) {
|
|
92
|
+
for (const sub of branch) {
|
|
93
|
+
this.strictifyRecursive(sub);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (o.not) {
|
|
98
|
+
this.strictifyRecursive(o.not);
|
|
99
|
+
}
|
|
100
|
+
if (o.items) {
|
|
101
|
+
if (Array.isArray(o.items)) {
|
|
102
|
+
for (const sub of o.items) {
|
|
103
|
+
this.strictifyRecursive(sub);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
this.strictifyRecursive(o.items);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const key of ["if", "then", "else"] as const) {
|
|
110
|
+
if (o[key]) {
|
|
111
|
+
this.strictifyRecursive(o[key]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const key of ["$defs", "definitions"] as const) {
|
|
115
|
+
const defs = o[key];
|
|
116
|
+
if (defs && typeof defs === "object" && !Array.isArray(defs)) {
|
|
117
|
+
for (const sub of Object.values(defs as Record<string, unknown>)) {
|
|
118
|
+
this.strictifyRecursive(sub);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./canvasIconName";
|
|
2
2
|
export * from "./chatModels/OpenAIChatModelFactory";
|
|
3
|
-
export * from "./chatModels/
|
|
3
|
+
export * from "./chatModels/OpenAiStrictJsonSchemaFactory";
|
|
4
4
|
export * from "./chatModels/OpenAiCredentialSession";
|
|
5
5
|
export * from "./chatModels/openAiChatModelConfig";
|
|
6
6
|
export * from "./chatModels/OpenAiChatModelPresetsFactory";
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import type { CredentialSessionService,
|
|
1
|
+
import type { CredentialSessionService, ZodSchemaAny } from "@codemation/core";
|
|
2
2
|
import { injectable } from "@codemation/core";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { toJsonSchema } from "@langchain/core/utils/json_schema";
|
|
6
|
-
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
7
|
-
import { toJSONSchema } from "zod/v4/core";
|
|
4
|
+
import { toJSONSchema as frameworkToJSONSchema } from "zod/v4/core";
|
|
8
5
|
|
|
9
6
|
import { ConnectionCredentialExecutionContextFactory } from "./ConnectionCredentialExecutionContextFactory";
|
|
10
|
-
import type { ResolvedTool } from "./aiAgentSupport.types";
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Shape of the instance-level `toJSONSchema` method that Zod v4 schemas expose. Conversions must go
|
|
10
|
+
* through this instance method (see {@link AIAgentExecutionHelpersFactory#createJsonSchemaRecord})
|
|
11
|
+
* rather than the module-level `toJSONSchema` import because the consumer's workflow-loader (see
|
|
12
|
+
* `CodemationConsumerConfigLoader.toNamespace`) can load Zod under a separate tsx namespace. That
|
|
13
|
+
* produces two runtime copies of Zod whose internal class / symbol identities don't overlap, so the
|
|
14
|
+
* framework-side module-level `toJSONSchema` throws "Cannot read properties of undefined (reading
|
|
15
|
+
* 'def')" on consumer-created schemas. The instance method is bound inside the schema's own module
|
|
16
|
+
* and therefore uses the matching Zod internals.
|
|
17
|
+
*/
|
|
18
|
+
type ZodInstanceToJsonSchema = (params?: Readonly<{ target: "draft-07" | "draft-7" | "draft-2020-12" }>) => unknown;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Helper utilities shared by {@link AIAgentNode} and supporting runners.
|
|
22
|
+
*
|
|
23
|
+
* Responsibilities:
|
|
24
|
+
* - {@link #createConnectionCredentialExecutionContextFactory} centralizes credential-context wiring.
|
|
25
|
+
* - {@link #createJsonSchemaRecord} is a pure Zod → draft-07 converter used by both
|
|
26
|
+
* `OpenAiStrictJsonSchemaFactory` (to feed OpenAI-strict structured output) and the
|
|
27
|
+
* `AgentStructuredOutputRepairPromptFactory` (to show a required-schema reminder).
|
|
15
28
|
*/
|
|
16
29
|
@injectable()
|
|
17
30
|
export class AIAgentExecutionHelpersFactory {
|
|
@@ -21,47 +34,14 @@ export class AIAgentExecutionHelpersFactory {
|
|
|
21
34
|
return new ConnectionCredentialExecutionContextFactory(credentialSessions);
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
createDynamicStructuredTool(
|
|
25
|
-
entry: ResolvedTool,
|
|
26
|
-
toolCredentialContext: NodeExecutionContext<any>,
|
|
27
|
-
item: Item,
|
|
28
|
-
itemIndex: number,
|
|
29
|
-
items: Items,
|
|
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.createJsonSchemaRecord(entry.runtime.inputSchema, {
|
|
37
|
-
schemaName: entry.config.name,
|
|
38
|
-
requireObjectRoot: true,
|
|
39
|
-
});
|
|
40
|
-
return new DynamicStructuredTool({
|
|
41
|
-
name: entry.config.name,
|
|
42
|
-
description: entry.config.description ?? entry.runtime.defaultDescription,
|
|
43
|
-
schema: schemaForOpenAi as unknown as ZodSchemaAny,
|
|
44
|
-
func: async (input) => {
|
|
45
|
-
const result = await entry.runtime.execute({
|
|
46
|
-
config: entry.config,
|
|
47
|
-
input,
|
|
48
|
-
ctx: toolCredentialContext,
|
|
49
|
-
item,
|
|
50
|
-
itemIndex,
|
|
51
|
-
items,
|
|
52
|
-
});
|
|
53
|
-
return JSON.stringify(result);
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
37
|
/**
|
|
59
|
-
* Produces a plain JSON Schema object
|
|
60
|
-
* -
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* -
|
|
38
|
+
* Produces a plain JSON Schema object (`draft-07`) from a Zod schema, as needed by
|
|
39
|
+
* OpenAI tool-parameter schemas and the structured-output repair prompt.
|
|
40
|
+
* - Prefers the schema's **instance** `toJSONSchema(...)` method so we stay inside the Zod
|
|
41
|
+
* instance that created the schema (works across consumer/framework tsx namespaces — see
|
|
42
|
+
* {@link ZodInstanceToJsonSchema}). Falls back to the framework-imported module function.
|
|
43
|
+
* - Strips root `$schema` (OpenAI ignores it).
|
|
44
|
+
* - Sanitizes `required` for cfworker json-schema compatibility (must be a string array or absent).
|
|
65
45
|
*/
|
|
66
46
|
createJsonSchemaRecord(
|
|
67
47
|
inputSchema: ZodSchemaAny,
|
|
@@ -71,20 +51,12 @@ export class AIAgentExecutionHelpersFactory {
|
|
|
71
51
|
}>,
|
|
72
52
|
): Record<string, unknown> {
|
|
73
53
|
const draft07Params = { target: "draft-07" as const };
|
|
74
|
-
|
|
75
|
-
if (isInteropZodSchema(inputSchema)) {
|
|
76
|
-
converted = toJSONSchema(inputSchema as unknown as Parameters<typeof toJSONSchema>[0], draft07Params);
|
|
77
|
-
} else {
|
|
78
|
-
converted = toJsonSchema(inputSchema);
|
|
79
|
-
if (isInteropZodSchema(converted)) {
|
|
80
|
-
converted = toJSONSchema(inputSchema as unknown as Parameters<typeof toJSONSchema>[0], draft07Params);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
54
|
+
const converted = this.convertZodSchemaToJsonSchema(inputSchema, draft07Params);
|
|
83
55
|
const record = converted as Record<string, unknown>;
|
|
84
56
|
const { $schema: _draftSchemaOmitted, ...rest } = record;
|
|
85
57
|
if (options.requireObjectRoot && rest.type !== "object") {
|
|
86
58
|
throw new Error(
|
|
87
|
-
`Cannot create
|
|
59
|
+
`Cannot create tool "${options.schemaName}": tool input schema must be a JSON Schema object type (got type=${String(rest.type)}).`,
|
|
88
60
|
);
|
|
89
61
|
}
|
|
90
62
|
if (
|
|
@@ -93,7 +65,7 @@ export class AIAgentExecutionHelpersFactory {
|
|
|
93
65
|
(typeof rest.properties !== "object" || Array.isArray(rest.properties))
|
|
94
66
|
) {
|
|
95
67
|
throw new Error(
|
|
96
|
-
`Cannot create
|
|
68
|
+
`Cannot create tool "${options.schemaName}": tool input schema "properties" must be an object (got ${JSON.stringify(rest.properties)}).`,
|
|
97
69
|
);
|
|
98
70
|
}
|
|
99
71
|
if (options.requireObjectRoot && rest.properties === undefined) {
|
|
@@ -103,6 +75,20 @@ export class AIAgentExecutionHelpersFactory {
|
|
|
103
75
|
return rest;
|
|
104
76
|
}
|
|
105
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Runs Zod's `toJSONSchema` via the schema's own instance method when available, so consumer
|
|
80
|
+
* schemas loaded under a different tsx namespace still convert correctly. If the caller handed us
|
|
81
|
+
* a payload that lacks that method (e.g. a plain JSON Schema record or a Zod instance whose
|
|
82
|
+
* prototype was stripped), we fall back to the framework-bundled module function.
|
|
83
|
+
*/
|
|
84
|
+
private convertZodSchemaToJsonSchema(inputSchema: ZodSchemaAny, params: Readonly<{ target: "draft-07" }>): unknown {
|
|
85
|
+
const candidate = (inputSchema as unknown as { toJSONSchema?: ZodInstanceToJsonSchema }).toJSONSchema;
|
|
86
|
+
if (typeof candidate === "function") {
|
|
87
|
+
return candidate.call(inputSchema, params);
|
|
88
|
+
}
|
|
89
|
+
return frameworkToJSONSchema(inputSchema as unknown as Parameters<typeof frameworkToJSONSchema>[0], params);
|
|
90
|
+
}
|
|
91
|
+
|
|
106
92
|
/**
|
|
107
93
|
* `@cfworker/json-schema` iterates `schema.required` with `for...of`; it must be a string array or absent.
|
|
108
94
|
*/
|