@codemation/core-nodes 0.4.3 → 1.0.1

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.4.3",
3
+ "version": "1.0.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -28,10 +28,11 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@langchain/core": "^1.1.31",
32
- "@langchain/openai": "^1.2.12",
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.8.1"
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 { ChatModelFactory, LangChainChatModelLike, NodeExecutionContext } from "@codemation/core";
1
+ import type { ChatLanguageModel, ChatModelFactory, NodeExecutionContext } from "@codemation/core";
2
2
  import { chatModel } from "@codemation/core";
3
- import { ChatOpenAI } from "@langchain/openai";
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<LangChainChatModelLike> {
13
+ ): Promise<ChatLanguageModel> {
12
14
  const session = await args.ctx.getCredential<OpenAiCredentialSession>(args.config.credentialSlotKey);
13
- return new ChatOpenAI({
15
+ const provider = createOpenAI({
14
16
  apiKey: session.apiKey,
15
- model: args.config.model,
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/OpenAIStructuredOutputMethodFactory";
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, Item, Items, NodeExecutionContext, ZodSchemaAny } from "@codemation/core";
1
+ import type { CredentialSessionService, 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";
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
- * LangChain adapters and credential context wiring for {@link AIAgentNode}.
14
- * Lives in a `*Factory.ts` composition-root module so construction stays explicit and testable.
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 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`.
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
- let converted: unknown;
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 LangChain tool "${options.schemaName}": tool input schema must be a JSON Schema object type (got type=${String(rest.type)}).`,
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 LangChain tool "${options.schemaName}": tool input schema "properties" must be an object (got ${JSON.stringify(rest.properties)}).`,
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
  */