@codemation/core-nodes 0.4.2 → 0.6.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 +222 -0
- package/dist/index.cjs +3485 -474
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1763 -685
- package/dist/index.d.ts +1763 -685
- package/dist/index.js +3452 -479
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/src/authoring/defineRestNode.types.ts +204 -0
- package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
- package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
- package/src/credentials/ApiKeyCredentialType.ts +60 -0
- package/src/credentials/BasicAuthCredentialType.ts +51 -0
- package/src/credentials/BearerTokenCredentialType.ts +40 -0
- package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
- package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
- package/src/credentials/index.ts +4 -0
- package/src/http/HttpBodyBuilder.ts +90 -0
- package/src/http/HttpRequestExecutor.ts +150 -0
- package/src/http/HttpUrlBuilder.ts +22 -0
- package/src/http/httpRequest.types.ts +69 -0
- package/src/index.ts +10 -1
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
- package/src/nodes/AIAgentNode.ts +391 -288
- package/src/nodes/AgentMessageFactory.ts +57 -49
- package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
- package/src/nodes/AgentToolExecutionCoordinator.ts +31 -16
- package/src/nodes/AssertionNode.ts +42 -0
- package/src/nodes/CronTriggerFactory.ts +45 -0
- package/src/nodes/CronTriggerNode.ts +40 -0
- package/src/nodes/HttpRequestNodeFactory.ts +99 -23
- package/src/nodes/IsTestRunNode.ts +25 -0
- package/src/nodes/NodeBackedToolRuntime.ts +40 -4
- package/src/nodes/TestTriggerNode.ts +33 -0
- package/src/nodes/WebhookTriggerFactory.ts +1 -1
- package/src/nodes/aggregate.ts +1 -1
- package/src/nodes/aiAgentSupport.types.ts +22 -2
- package/src/nodes/assertion.ts +42 -0
- package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
- package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
- package/src/nodes/collections/collectionGetNode.types.ts +26 -0
- package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
- package/src/nodes/collections/collectionListNode.types.ts +30 -0
- package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
- package/src/nodes/collections/index.ts +6 -0
- package/src/nodes/httpRequest.ts +62 -1
- package/src/nodes/if.ts +1 -1
- package/src/nodes/isTestRun.ts +24 -0
- package/src/nodes/mapData.ts +1 -0
- package/src/nodes/merge.ts +1 -1
- package/src/nodes/noOp.ts +1 -0
- package/src/nodes/split.ts +1 -1
- package/src/nodes/testTrigger.ts +72 -0
- package/src/nodes/wait.ts +1 -0
- package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performs an OAuth2 `client_credentials` token exchange against a token endpoint
|
|
3
|
+
* and returns the resulting access token.
|
|
4
|
+
*
|
|
5
|
+
* Lives in a Factory file so the body URLSearchParams construction is allowed at
|
|
6
|
+
* the composition root.
|
|
7
|
+
*/
|
|
8
|
+
export type OAuth2ClientCredentialsArgs = Readonly<{
|
|
9
|
+
tokenUrl: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
scopes: string;
|
|
13
|
+
audience: string;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
export class OAuth2TokenExchangeFactory {
|
|
17
|
+
async create(args: OAuth2ClientCredentialsArgs): Promise<string> {
|
|
18
|
+
const body = new URLSearchParams({
|
|
19
|
+
grant_type: "client_credentials",
|
|
20
|
+
client_id: args.clientId,
|
|
21
|
+
});
|
|
22
|
+
if (args.scopes) {
|
|
23
|
+
body.set("scope", args.scopes);
|
|
24
|
+
}
|
|
25
|
+
if (args.audience) {
|
|
26
|
+
body.set("audience", args.audience);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const encoded = Buffer.from(`${args.clientId}:${args.clientSecret}`).toString("base64");
|
|
30
|
+
|
|
31
|
+
const response = await globalThis.fetch(args.tokenUrl, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
35
|
+
authorization: `Basic ${encoded}`,
|
|
36
|
+
},
|
|
37
|
+
body: body.toString(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const text = await response.text().catch(() => "");
|
|
42
|
+
throw new Error(`Token exchange failed (${response.status} ${response.statusText}): ${text}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const json = (await response.json()) as Record<string, unknown>;
|
|
46
|
+
const token = String(json["access_token"] ?? "");
|
|
47
|
+
if (!token) {
|
|
48
|
+
throw new Error("Token exchange response did not include an access_token.");
|
|
49
|
+
}
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { apiKeyCredentialType } from "./ApiKeyCredentialType";
|
|
2
|
+
export { basicAuthCredentialType } from "./BasicAuthCredentialType";
|
|
3
|
+
export { bearerTokenCredentialType } from "./BearerTokenCredentialType";
|
|
4
|
+
export { oauth2ClientCredentialsType } from "./OAuth2ClientCredentialsTypeFactory";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Item, NodeExecutionContext } from "@codemation/core";
|
|
2
|
+
import type { RunnableNodeConfig } from "@codemation/core";
|
|
3
|
+
import type { HttpBodySpec } from "./httpRequest.types";
|
|
4
|
+
|
|
5
|
+
export type EncodedBody = Readonly<{
|
|
6
|
+
body: NonNullable<RequestInit["body"]>;
|
|
7
|
+
/**
|
|
8
|
+
* Desired Content-Type header. Empty string means `fetch` should set it automatically
|
|
9
|
+
* (used for multipart/form-data so the boundary is set correctly by the browser/Node runtime).
|
|
10
|
+
*/
|
|
11
|
+
contentType: string;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds a fetch-compatible `BodyInit` + Content-Type pair from an {@link HttpBodySpec}.
|
|
16
|
+
* Multipart binaries are read from `item.binary` via `ctx.binary.openReadStream`.
|
|
17
|
+
*/
|
|
18
|
+
export class HttpBodyBuilder {
|
|
19
|
+
async build(
|
|
20
|
+
spec: HttpBodySpec | undefined,
|
|
21
|
+
item: Item,
|
|
22
|
+
ctx: NodeExecutionContext<RunnableNodeConfig<unknown, unknown>>,
|
|
23
|
+
): Promise<EncodedBody | undefined> {
|
|
24
|
+
if (!spec || spec.kind === "none") {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (spec.kind === "json") {
|
|
29
|
+
return {
|
|
30
|
+
body: JSON.stringify(spec.data),
|
|
31
|
+
contentType: "application/json",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (spec.kind === "form") {
|
|
36
|
+
const params = new URLSearchParams();
|
|
37
|
+
for (const [key, value] of Object.entries(spec.data)) {
|
|
38
|
+
params.append(key, value);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
body: params.toString(),
|
|
42
|
+
contentType: "application/x-www-form-urlencoded",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (spec.kind === "multipart") {
|
|
47
|
+
const formData = new FormData();
|
|
48
|
+
for (const [key, value] of Object.entries(spec.fields)) {
|
|
49
|
+
formData.append(key, value);
|
|
50
|
+
}
|
|
51
|
+
if (spec.binaries) {
|
|
52
|
+
for (const [fieldName, binaryRef] of Object.entries(spec.binaries)) {
|
|
53
|
+
const attachment = item.binary?.[binaryRef];
|
|
54
|
+
if (attachment) {
|
|
55
|
+
const readResult = await ctx.binary.openReadStream(attachment);
|
|
56
|
+
if (readResult) {
|
|
57
|
+
const reader = readResult.body.getReader();
|
|
58
|
+
const chunks: Uint8Array[] = [];
|
|
59
|
+
let done = false;
|
|
60
|
+
while (!done) {
|
|
61
|
+
const result = await reader.read();
|
|
62
|
+
done = result.done;
|
|
63
|
+
if (result.value) {
|
|
64
|
+
chunks.push(result.value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
68
|
+
const merged = new Uint8Array(totalLength);
|
|
69
|
+
let offset = 0;
|
|
70
|
+
for (const chunk of chunks) {
|
|
71
|
+
merged.set(chunk, offset);
|
|
72
|
+
offset += chunk.length;
|
|
73
|
+
}
|
|
74
|
+
const blob = new Blob([merged], { type: attachment.mimeType });
|
|
75
|
+
formData.append(fieldName, blob, attachment.filename ?? binaryRef);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// FormData sets its own Content-Type with boundary; empty string signals that
|
|
81
|
+
// fetch should set it automatically.
|
|
82
|
+
return {
|
|
83
|
+
body: formData,
|
|
84
|
+
contentType: "",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Item } from "@codemation/core";
|
|
2
|
+
import type { HttpRequestResult, HttpRequestSpec } from "./httpRequest.types";
|
|
3
|
+
import type { HttpBodyBuilder } from "./HttpBodyBuilder";
|
|
4
|
+
import type { HttpUrlBuilder } from "./HttpUrlBuilder";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Executes a single HTTP request described by {@link HttpRequestSpec}.
|
|
8
|
+
*
|
|
9
|
+
* - Credential sessions provide header/query deltas via `applyToRequest`.
|
|
10
|
+
* - Body encoding is delegated to {@link HttpBodyBuilder}.
|
|
11
|
+
* - URL query merging is delegated to {@link HttpUrlBuilder}.
|
|
12
|
+
* - Binary response bodies: when `download.mode` triggers binary attach, the
|
|
13
|
+
* `bodyBinaryName` field is set in the result but the body is NOT read here.
|
|
14
|
+
* Callers that need binary attachment should use `buildRequest` to get the
|
|
15
|
+
* resolved URL + init and make the fetch + binary attach themselves.
|
|
16
|
+
*
|
|
17
|
+
* Collaborators (`fetch`, body builder, url builder) are injected so callers
|
|
18
|
+
* own construction at composition roots and tests can supply deterministic stubs.
|
|
19
|
+
*/
|
|
20
|
+
export class HttpRequestExecutor {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly fetchFn: typeof globalThis.fetch,
|
|
23
|
+
private readonly bodyBuilder: HttpBodyBuilder,
|
|
24
|
+
private readonly urlBuilder: HttpUrlBuilder,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the fetch init (headers, query, body) from the spec + credential delta,
|
|
29
|
+
* returning both the resolved URL and the RequestInit so callers can make the
|
|
30
|
+
* actual fetch call themselves (useful for streaming / binary attach).
|
|
31
|
+
*/
|
|
32
|
+
async buildRequest(spec: HttpRequestSpec, item: Item): Promise<Readonly<{ url: string; init: RequestInit }>> {
|
|
33
|
+
const credentialDelta = spec.credential?.applyToRequest(spec) ?? {};
|
|
34
|
+
|
|
35
|
+
const mergedHeaders: Record<string, string> = {
|
|
36
|
+
...(spec.headers ?? {}),
|
|
37
|
+
...(credentialDelta.headers ?? {}),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mergedQuery: Record<string, string | string[]> = {
|
|
41
|
+
...(spec.query ?? {}),
|
|
42
|
+
...(credentialDelta.query ?? {}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const encodedBody = await this.bodyBuilder.build(spec.body, item, spec.ctx);
|
|
46
|
+
|
|
47
|
+
// Only set Content-Type from the encoded body when it is non-empty
|
|
48
|
+
// (empty string = FormData will set it automatically).
|
|
49
|
+
if (encodedBody && encodedBody.contentType) {
|
|
50
|
+
mergedHeaders["content-type"] = encodedBody.contentType;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resolvedUrl = this.urlBuilder.build(spec.url, mergedQuery);
|
|
54
|
+
|
|
55
|
+
const init: RequestInit = {
|
|
56
|
+
method: spec.method,
|
|
57
|
+
headers: mergedHeaders,
|
|
58
|
+
...(encodedBody ? { body: encodedBody.body } : {}),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return { url: resolvedUrl, init };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Executes an HTTP request and returns parsed result.
|
|
66
|
+
* For binary downloads (when `shouldAttachBody` is true), the body is NOT consumed
|
|
67
|
+
* and callers must call `ctx.binary.attach` directly using the resolved URL + init
|
|
68
|
+
* (available via `buildRequest`).
|
|
69
|
+
*/
|
|
70
|
+
async execute(spec: HttpRequestSpec, item: Item): Promise<HttpRequestResult> {
|
|
71
|
+
const { url: resolvedUrl, init } = await this.buildRequest(spec, item);
|
|
72
|
+
|
|
73
|
+
const response = await this.fetchFn(resolvedUrl, init);
|
|
74
|
+
|
|
75
|
+
const responseHeaders = this.readHeaders(response.headers);
|
|
76
|
+
const mimeType = this.resolveMimeType(responseHeaders);
|
|
77
|
+
|
|
78
|
+
const downloadMode = spec.download?.mode ?? "auto";
|
|
79
|
+
const binaryName = spec.download?.binaryName ?? "body";
|
|
80
|
+
const shouldDownload = this.shouldAttachBody(downloadMode, mimeType);
|
|
81
|
+
|
|
82
|
+
const isJson = this.isJsonMimeType(mimeType);
|
|
83
|
+
|
|
84
|
+
let json: unknown | undefined;
|
|
85
|
+
let text: string | undefined;
|
|
86
|
+
let bodyBinaryName: string | undefined;
|
|
87
|
+
|
|
88
|
+
if (shouldDownload) {
|
|
89
|
+
// Signal to caller that binary attachment is needed.
|
|
90
|
+
bodyBinaryName = binaryName;
|
|
91
|
+
// Do NOT read the body here — the caller must handle binary attach separately.
|
|
92
|
+
} else if (isJson) {
|
|
93
|
+
try {
|
|
94
|
+
json = await response.json();
|
|
95
|
+
} catch {
|
|
96
|
+
text = await response.text();
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
text = await response.text();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
url: resolvedUrl,
|
|
104
|
+
method: spec.method.toUpperCase(),
|
|
105
|
+
status: response.status,
|
|
106
|
+
ok: response.ok,
|
|
107
|
+
statusText: response.statusText,
|
|
108
|
+
mimeType,
|
|
109
|
+
headers: responseHeaders,
|
|
110
|
+
...(json !== undefined ? { json } : {}),
|
|
111
|
+
...(text !== undefined ? { text } : {}),
|
|
112
|
+
...(bodyBinaryName !== undefined ? { bodyBinaryName } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private readHeaders(headers: Headers): Readonly<Record<string, string>> {
|
|
117
|
+
const values: Record<string, string> = {};
|
|
118
|
+
headers.forEach((value, key) => {
|
|
119
|
+
values[key] = value;
|
|
120
|
+
});
|
|
121
|
+
return values;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private resolveMimeType(headers: Readonly<Record<string, string>>): string {
|
|
125
|
+
const contentType = headers["content-type"];
|
|
126
|
+
if (!contentType) {
|
|
127
|
+
return "application/octet-stream";
|
|
128
|
+
}
|
|
129
|
+
return contentType.split(";")[0]?.trim() || "application/octet-stream";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private isJsonMimeType(mimeType: string): boolean {
|
|
133
|
+
return mimeType === "application/json" || mimeType.endsWith("+json");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private shouldAttachBody(mode: "auto" | "always" | "never", mimeType: string): boolean {
|
|
137
|
+
if (mode === "always") {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (mode === "never") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
mimeType.startsWith("image/") ||
|
|
145
|
+
mimeType.startsWith("audio/") ||
|
|
146
|
+
mimeType.startsWith("video/") ||
|
|
147
|
+
mimeType === "application/pdf"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merges query parameters into a base URL.
|
|
3
|
+
* Handles both scalar and array values, and preserves any existing params.
|
|
4
|
+
*/
|
|
5
|
+
export class HttpUrlBuilder {
|
|
6
|
+
build(baseUrl: string, query?: Readonly<Record<string, string | string[]>>): string {
|
|
7
|
+
if (!query || Object.keys(query).length === 0) {
|
|
8
|
+
return baseUrl;
|
|
9
|
+
}
|
|
10
|
+
const parsed = new URL(baseUrl);
|
|
11
|
+
for (const [key, value] of Object.entries(query)) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
for (const entry of value) {
|
|
14
|
+
parsed.searchParams.append(key, entry);
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
parsed.searchParams.append(key, value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return parsed.toString();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { NodeExecutionContext } from "@codemation/core";
|
|
2
|
+
import type { RunnableNodeConfig } from "@codemation/core";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Binary reference key into `item.binary`.
|
|
6
|
+
*/
|
|
7
|
+
export type BinaryRef = string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Discriminated union for the HTTP request body.
|
|
11
|
+
*/
|
|
12
|
+
export type HttpBodySpec =
|
|
13
|
+
| Readonly<{ kind: "none" }>
|
|
14
|
+
| Readonly<{ kind: "json"; data: unknown }>
|
|
15
|
+
| Readonly<{ kind: "form"; data: Readonly<Record<string, string>> }>
|
|
16
|
+
| Readonly<{
|
|
17
|
+
kind: "multipart";
|
|
18
|
+
fields: Readonly<Record<string, string>>;
|
|
19
|
+
binaries?: Readonly<Record<string, BinaryRef>>;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Session interface that credential types implement.
|
|
24
|
+
* Returns header/query deltas so the executor can merge them without
|
|
25
|
+
* mutating the immutable HttpRequestSpec.
|
|
26
|
+
*/
|
|
27
|
+
export interface CredentialSession {
|
|
28
|
+
applyToRequest(spec: HttpRequestSpec): HttpCredentialDelta;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mutations the credential session wants to apply to the outgoing request.
|
|
33
|
+
*/
|
|
34
|
+
export type HttpCredentialDelta = Readonly<{
|
|
35
|
+
headers?: Readonly<Record<string, string>>;
|
|
36
|
+
query?: Readonly<Record<string, string>>;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Full specification of one HTTP request. All URLs are fully resolved before
|
|
41
|
+
* being passed here (template substitution already applied by the caller).
|
|
42
|
+
*/
|
|
43
|
+
export type HttpRequestSpec = Readonly<{
|
|
44
|
+
url: string;
|
|
45
|
+
method: string;
|
|
46
|
+
headers?: Readonly<Record<string, string>>;
|
|
47
|
+
query?: Readonly<Record<string, string | string[]>>;
|
|
48
|
+
body?: HttpBodySpec;
|
|
49
|
+
credential?: CredentialSession;
|
|
50
|
+
download?: Readonly<{ mode: "auto" | "always" | "never"; binaryName: string }>;
|
|
51
|
+
/** Execution context — needed for binary attach. */
|
|
52
|
+
ctx: NodeExecutionContext<RunnableNodeConfig<unknown, unknown>>;
|
|
53
|
+
}>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result of executing an HTTP request.
|
|
57
|
+
*/
|
|
58
|
+
export type HttpRequestResult = Readonly<{
|
|
59
|
+
url: string;
|
|
60
|
+
method: string;
|
|
61
|
+
status: number;
|
|
62
|
+
ok: boolean;
|
|
63
|
+
statusText: string;
|
|
64
|
+
mimeType: string;
|
|
65
|
+
headers: Readonly<Record<string, string>>;
|
|
66
|
+
json?: unknown;
|
|
67
|
+
text?: string;
|
|
68
|
+
bodyBinaryName?: string;
|
|
69
|
+
}>;
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
export * from "./canvasIconName";
|
|
2
|
+
export * from "./credentials/index";
|
|
3
|
+
export * from "./http/httpRequest.types";
|
|
4
|
+
export * from "./authoring/defineRestNode.types";
|
|
2
5
|
export * from "./chatModels/OpenAIChatModelFactory";
|
|
3
|
-
export * from "./chatModels/
|
|
6
|
+
export * from "./chatModels/OpenAiStrictJsonSchemaFactory";
|
|
4
7
|
export * from "./chatModels/OpenAiCredentialSession";
|
|
5
8
|
export * from "./chatModels/openAiChatModelConfig";
|
|
6
9
|
export * from "./chatModels/OpenAiChatModelPresetsFactory";
|
|
7
10
|
export * from "./nodes/aiAgent";
|
|
11
|
+
export * from "./nodes/assertion";
|
|
8
12
|
export * from "./nodes/CallbackNodeFactory";
|
|
9
13
|
export * from "./nodes/httpRequest";
|
|
10
14
|
export * from "./nodes/aggregate";
|
|
11
15
|
export * from "./nodes/filter";
|
|
12
16
|
export * from "./nodes/if";
|
|
17
|
+
export * from "./nodes/isTestRun";
|
|
13
18
|
export * from "./nodes/switch";
|
|
14
19
|
export * from "./nodes/split";
|
|
20
|
+
export * from "./nodes/CronTriggerFactory";
|
|
21
|
+
export * from "./nodes/CronTriggerNode";
|
|
15
22
|
export * from "./nodes/ManualTriggerFactory";
|
|
16
23
|
export * from "./nodes/mapData";
|
|
17
24
|
export * from "./nodes/merge";
|
|
18
25
|
export * from "./nodes/noOp";
|
|
19
26
|
export * from "./nodes/subWorkflow";
|
|
27
|
+
export * from "./nodes/testTrigger";
|
|
20
28
|
export * from "./nodes/wait";
|
|
21
29
|
export * from "./nodes/webhookRespondNowAndContinueError";
|
|
22
30
|
export * from "./nodes/webhookRespondNowError";
|
|
@@ -30,3 +38,4 @@ export * from "./nodes/ConnectionCredentialNode";
|
|
|
30
38
|
export * from "./nodes/ConnectionCredentialNodeConfig";
|
|
31
39
|
export * from "./nodes/ConnectionCredentialNodeConfigFactory";
|
|
32
40
|
export * from "./nodes/ConnectionCredentialExecutionContextFactory";
|
|
41
|
+
export * from "./nodes/collections";
|
|
@@ -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
|
*/
|