@codemation/core-nodes 1.0.1 → 1.1.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 +130 -0
- package/dist/index.cjs +3002 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1521 -551
- package/dist/index.d.ts +1521 -551
- package/dist/index.js +2969 -73
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/authoring/defineRestNode.types.ts +204 -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 +9 -0
- package/src/nodes/AIAgentNode.ts +101 -3
- package/src/nodes/AgentToolExecutionCoordinator.ts +29 -3
- 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/aiAgentSupport.types.ts +18 -3
- 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 +61 -1
- package/src/nodes/isTestRun.ts +24 -0
- package/src/nodes/testTrigger.ts +72 -0
|
@@ -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
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";
|
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -356,7 +356,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
356
356
|
return {
|
|
357
357
|
config: entry.config,
|
|
358
358
|
inputSchema: entry.runtime.inputSchema,
|
|
359
|
-
execute: async (input
|
|
359
|
+
execute: async (input, hooks): Promise<unknown> => {
|
|
360
360
|
const validated = entry.runtime.inputSchema.parse(input) as unknown;
|
|
361
361
|
return await entry.runtime.execute({
|
|
362
362
|
config: entry.config,
|
|
@@ -365,6 +365,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
365
365
|
item,
|
|
366
366
|
itemIndex,
|
|
367
367
|
items,
|
|
368
|
+
hooks,
|
|
368
369
|
});
|
|
369
370
|
},
|
|
370
371
|
} satisfies ItemScopedToolBinding;
|
|
@@ -421,11 +422,36 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
421
422
|
activationId: ctx.activationId,
|
|
422
423
|
inputsByPort: itemInputsByPort,
|
|
423
424
|
});
|
|
425
|
+
await ctx.nodeState?.appendConnectionInvocation({
|
|
426
|
+
invocationId,
|
|
427
|
+
connectionNodeId: languageModelConnectionNodeId,
|
|
428
|
+
parentAgentNodeId: ctx.nodeId,
|
|
429
|
+
parentAgentActivationId: ctx.activationId,
|
|
430
|
+
status: "queued",
|
|
431
|
+
managedInput: summarizedInput,
|
|
432
|
+
queuedAt: startedAt.toISOString(),
|
|
433
|
+
iterationId: ctx.iterationId,
|
|
434
|
+
itemIndex: ctx.itemIndex,
|
|
435
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
436
|
+
});
|
|
424
437
|
await ctx.nodeState?.markRunning({
|
|
425
438
|
nodeId: languageModelConnectionNodeId,
|
|
426
439
|
activationId: ctx.activationId,
|
|
427
440
|
inputsByPort: itemInputsByPort,
|
|
428
441
|
});
|
|
442
|
+
await ctx.nodeState?.appendConnectionInvocation({
|
|
443
|
+
invocationId,
|
|
444
|
+
connectionNodeId: languageModelConnectionNodeId,
|
|
445
|
+
parentAgentNodeId: ctx.nodeId,
|
|
446
|
+
parentAgentActivationId: ctx.activationId,
|
|
447
|
+
status: "running",
|
|
448
|
+
managedInput: summarizedInput,
|
|
449
|
+
queuedAt: startedAt.toISOString(),
|
|
450
|
+
startedAt: startedAt.toISOString(),
|
|
451
|
+
iterationId: ctx.iterationId,
|
|
452
|
+
itemIndex: ctx.itemIndex,
|
|
453
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
454
|
+
});
|
|
429
455
|
try {
|
|
430
456
|
const tools = this.buildToolSet(itemScopedTools);
|
|
431
457
|
const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
|
|
@@ -441,11 +467,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
441
467
|
});
|
|
442
468
|
const turnResult = this.extractTurnResult(result as AnyGenerateTextResult);
|
|
443
469
|
const finishedAt = new Date();
|
|
470
|
+
const managedOutput = this.summarizeTurnOutput(turnResult);
|
|
444
471
|
await ctx.nodeState?.markCompleted({
|
|
445
472
|
nodeId: languageModelConnectionNodeId,
|
|
446
473
|
activationId: ctx.activationId,
|
|
447
474
|
inputsByPort: itemInputsByPort,
|
|
448
|
-
outputs: AgentOutputFactory.fromUnknown(
|
|
475
|
+
outputs: AgentOutputFactory.fromUnknown(managedOutput),
|
|
449
476
|
});
|
|
450
477
|
await span.attachArtifact({ kind: "ai.messages", contentType: "application/json", previewJson: summarizedInput });
|
|
451
478
|
await span.attachArtifact({ kind: "ai.response", contentType: "application/json", previewJson: turnResult.text });
|
|
@@ -458,10 +485,13 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
458
485
|
parentAgentActivationId: ctx.activationId,
|
|
459
486
|
status: "completed",
|
|
460
487
|
managedInput: summarizedInput,
|
|
461
|
-
managedOutput
|
|
488
|
+
managedOutput,
|
|
462
489
|
queuedAt: startedAt.toISOString(),
|
|
463
490
|
startedAt: startedAt.toISOString(),
|
|
464
491
|
finishedAt: finishedAt.toISOString(),
|
|
492
|
+
iterationId: ctx.iterationId,
|
|
493
|
+
itemIndex: ctx.itemIndex,
|
|
494
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
465
495
|
});
|
|
466
496
|
return turnResult;
|
|
467
497
|
} catch (error) {
|
|
@@ -504,11 +534,36 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
504
534
|
activationId: ctx.activationId,
|
|
505
535
|
inputsByPort: itemInputsByPort,
|
|
506
536
|
});
|
|
537
|
+
await ctx.nodeState?.appendConnectionInvocation({
|
|
538
|
+
invocationId,
|
|
539
|
+
connectionNodeId: languageModelConnectionNodeId,
|
|
540
|
+
parentAgentNodeId: ctx.nodeId,
|
|
541
|
+
parentAgentActivationId: ctx.activationId,
|
|
542
|
+
status: "queued",
|
|
543
|
+
managedInput: summarizedInput,
|
|
544
|
+
queuedAt: startedAt.toISOString(),
|
|
545
|
+
iterationId: ctx.iterationId,
|
|
546
|
+
itemIndex: ctx.itemIndex,
|
|
547
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
548
|
+
});
|
|
507
549
|
await ctx.nodeState?.markRunning({
|
|
508
550
|
nodeId: languageModelConnectionNodeId,
|
|
509
551
|
activationId: ctx.activationId,
|
|
510
552
|
inputsByPort: itemInputsByPort,
|
|
511
553
|
});
|
|
554
|
+
await ctx.nodeState?.appendConnectionInvocation({
|
|
555
|
+
invocationId,
|
|
556
|
+
connectionNodeId: languageModelConnectionNodeId,
|
|
557
|
+
parentAgentNodeId: ctx.nodeId,
|
|
558
|
+
parentAgentActivationId: ctx.activationId,
|
|
559
|
+
status: "running",
|
|
560
|
+
managedInput: summarizedInput,
|
|
561
|
+
queuedAt: startedAt.toISOString(),
|
|
562
|
+
startedAt: startedAt.toISOString(),
|
|
563
|
+
iterationId: ctx.iterationId,
|
|
564
|
+
itemIndex: ctx.itemIndex,
|
|
565
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
566
|
+
});
|
|
512
567
|
try {
|
|
513
568
|
const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
|
|
514
569
|
const outputSchema =
|
|
@@ -551,6 +606,9 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
551
606
|
queuedAt: startedAt.toISOString(),
|
|
552
607
|
startedAt: startedAt.toISOString(),
|
|
553
608
|
finishedAt: finishedAt.toISOString(),
|
|
609
|
+
iterationId: ctx.iterationId,
|
|
610
|
+
itemIndex: ctx.itemIndex,
|
|
611
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
554
612
|
});
|
|
555
613
|
return result.experimental_output;
|
|
556
614
|
} catch (error) {
|
|
@@ -589,6 +647,22 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
589
647
|
};
|
|
590
648
|
}
|
|
591
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Build a no-code-friendly output payload for an LLM round.
|
|
652
|
+
*
|
|
653
|
+
* Always includes `content` (matching the canvas snapshot shape used elsewhere) and adds a
|
|
654
|
+
* `toolCalls` array when the round produced tool calls so the execution inspector surfaces the
|
|
655
|
+
* planned calls instead of just an empty `""` for tool-only rounds.
|
|
656
|
+
*/
|
|
657
|
+
private summarizeTurnOutput(turnResult: TurnResult): JsonValue {
|
|
658
|
+
if (turnResult.toolCalls.length === 0) return { content: turnResult.text };
|
|
659
|
+
const toolCalls = turnResult.toolCalls.map((toolCall) => ({
|
|
660
|
+
name: toolCall.name,
|
|
661
|
+
args: this.resultToJsonValue(toolCall.input) ?? null,
|
|
662
|
+
}));
|
|
663
|
+
return { content: turnResult.text, toolCalls };
|
|
664
|
+
}
|
|
665
|
+
|
|
592
666
|
private extractTurnResult(result: AnyGenerateTextResult): TurnResult {
|
|
593
667
|
const usage = this.extractUsageFromResult(result);
|
|
594
668
|
const text = result.text;
|
|
@@ -642,6 +716,13 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
642
716
|
[CodemationTelemetryAttributeNames.connectionInvocationId]: invocationId,
|
|
643
717
|
[GenAiTelemetryAttributeNames.operationName]: "chat",
|
|
644
718
|
[GenAiTelemetryAttributeNames.requestModel]: this.resolveChatModelName(ctx.config.chatModel),
|
|
719
|
+
...(ctx.iterationId ? { [CodemationTelemetryAttributeNames.iterationId]: ctx.iterationId } : {}),
|
|
720
|
+
...(typeof ctx.itemIndex === "number"
|
|
721
|
+
? { [CodemationTelemetryAttributeNames.iterationIndex]: ctx.itemIndex }
|
|
722
|
+
: {}),
|
|
723
|
+
...(ctx.parentInvocationId
|
|
724
|
+
? { [CodemationTelemetryAttributeNames.parentInvocationId]: ctx.parentInvocationId }
|
|
725
|
+
: {}),
|
|
645
726
|
},
|
|
646
727
|
});
|
|
647
728
|
}
|
|
@@ -707,12 +788,25 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
707
788
|
plannedToolCalls: ReadonlyArray<PlannedToolCall>,
|
|
708
789
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
709
790
|
): Promise<void> {
|
|
791
|
+
const queuedAt = new Date().toISOString();
|
|
710
792
|
for (const plannedToolCall of plannedToolCalls) {
|
|
711
793
|
await ctx.nodeState?.markQueued({
|
|
712
794
|
nodeId: plannedToolCall.nodeId,
|
|
713
795
|
activationId: ctx.activationId,
|
|
714
796
|
inputsByPort: AgentToolCallPortMap.fromInput(plannedToolCall.toolCall.input ?? {}),
|
|
715
797
|
});
|
|
798
|
+
await ctx.nodeState?.appendConnectionInvocation({
|
|
799
|
+
invocationId: plannedToolCall.invocationId,
|
|
800
|
+
connectionNodeId: plannedToolCall.nodeId,
|
|
801
|
+
parentAgentNodeId: ctx.nodeId,
|
|
802
|
+
parentAgentActivationId: ctx.activationId,
|
|
803
|
+
status: "queued",
|
|
804
|
+
managedInput: this.resultToJsonValue(plannedToolCall.toolCall.input),
|
|
805
|
+
queuedAt,
|
|
806
|
+
iterationId: ctx.iterationId,
|
|
807
|
+
itemIndex: ctx.itemIndex,
|
|
808
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
809
|
+
});
|
|
716
810
|
}
|
|
717
811
|
}
|
|
718
812
|
|
|
@@ -732,6 +826,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
732
826
|
toolCall,
|
|
733
827
|
invocationIndex,
|
|
734
828
|
nodeId: ConnectionNodeIdFactory.toolConnectionNodeId(parentNodeId, binding.config.name),
|
|
829
|
+
invocationId: ConnectionInvocationIdFactory.create(),
|
|
735
830
|
} satisfies PlannedToolCall;
|
|
736
831
|
});
|
|
737
832
|
}
|
|
@@ -771,6 +866,9 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
771
866
|
queuedAt: args.startedAt.toISOString(),
|
|
772
867
|
startedAt: args.startedAt.toISOString(),
|
|
773
868
|
finishedAt: finishedAt.toISOString(),
|
|
869
|
+
iterationId: args.ctx.iterationId,
|
|
870
|
+
itemIndex: args.ctx.itemIndex,
|
|
871
|
+
parentInvocationId: args.ctx.parentInvocationId,
|
|
774
872
|
});
|
|
775
873
|
return effectiveError;
|
|
776
874
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonValue, NodeExecutionContext } from "@codemation/core";
|
|
2
|
-
import { CodemationTelemetryAttributeNames,
|
|
2
|
+
import { CodemationTelemetryAttributeNames, inject, injectable } from "@codemation/core";
|
|
3
3
|
|
|
4
4
|
import type { AIAgent } from "./AIAgentConfig";
|
|
5
5
|
import { AgentOutputFactory } from "./AgentOutputFactory";
|
|
@@ -53,7 +53,7 @@ export class AgentToolExecutionCoordinator {
|
|
|
53
53
|
): Promise<ExecutedToolCall> {
|
|
54
54
|
const { plannedToolCall, ctx } = args;
|
|
55
55
|
const toolCallInputsByPort = AgentToolCallPortMap.fromInput(plannedToolCall.toolCall.input ?? {});
|
|
56
|
-
const invocationId =
|
|
56
|
+
const invocationId = plannedToolCall.invocationId;
|
|
57
57
|
const startedAt = new Date();
|
|
58
58
|
const span = ctx.telemetry.startChildSpan({
|
|
59
59
|
name: "agent.tool.call",
|
|
@@ -62,6 +62,13 @@ export class AgentToolExecutionCoordinator {
|
|
|
62
62
|
attributes: {
|
|
63
63
|
[CodemationTelemetryAttributeNames.connectionInvocationId]: invocationId,
|
|
64
64
|
[CodemationTelemetryAttributeNames.toolName]: plannedToolCall.binding.config.name,
|
|
65
|
+
...(ctx.iterationId ? { [CodemationTelemetryAttributeNames.iterationId]: ctx.iterationId } : {}),
|
|
66
|
+
...(typeof ctx.itemIndex === "number"
|
|
67
|
+
? { [CodemationTelemetryAttributeNames.iterationIndex]: ctx.itemIndex }
|
|
68
|
+
: {}),
|
|
69
|
+
...(ctx.parentInvocationId
|
|
70
|
+
? { [CodemationTelemetryAttributeNames.parentInvocationId]: ctx.parentInvocationId }
|
|
71
|
+
: {}),
|
|
65
72
|
},
|
|
66
73
|
});
|
|
67
74
|
await ctx.nodeState?.markRunning({
|
|
@@ -69,9 +76,24 @@ export class AgentToolExecutionCoordinator {
|
|
|
69
76
|
activationId: ctx.activationId,
|
|
70
77
|
inputsByPort: toolCallInputsByPort,
|
|
71
78
|
});
|
|
79
|
+
await ctx.nodeState?.appendConnectionInvocation({
|
|
80
|
+
invocationId,
|
|
81
|
+
connectionNodeId: plannedToolCall.nodeId,
|
|
82
|
+
parentAgentNodeId: ctx.nodeId,
|
|
83
|
+
parentAgentActivationId: ctx.activationId,
|
|
84
|
+
status: "running",
|
|
85
|
+
managedInput: this.toJsonValue(plannedToolCall.toolCall.input),
|
|
86
|
+
queuedAt: startedAt.toISOString(),
|
|
87
|
+
startedAt: startedAt.toISOString(),
|
|
88
|
+
iterationId: ctx.iterationId,
|
|
89
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
90
|
+
});
|
|
72
91
|
|
|
73
92
|
try {
|
|
74
|
-
const result = await plannedToolCall.binding.execute(plannedToolCall.toolCall.input ?? {}
|
|
93
|
+
const result = await plannedToolCall.binding.execute(plannedToolCall.toolCall.input ?? {}, {
|
|
94
|
+
parentSpan: span,
|
|
95
|
+
parentInvocationId: invocationId,
|
|
96
|
+
});
|
|
75
97
|
const serialized = typeof result === "string" ? result : JSON.stringify(result);
|
|
76
98
|
const finishedAt = new Date();
|
|
77
99
|
await ctx.nodeState?.markCompleted({
|
|
@@ -102,6 +124,8 @@ export class AgentToolExecutionCoordinator {
|
|
|
102
124
|
queuedAt: startedAt.toISOString(),
|
|
103
125
|
startedAt: startedAt.toISOString(),
|
|
104
126
|
finishedAt: finishedAt.toISOString(),
|
|
127
|
+
iterationId: ctx.iterationId,
|
|
128
|
+
parentInvocationId: ctx.parentInvocationId,
|
|
105
129
|
});
|
|
106
130
|
return {
|
|
107
131
|
toolName: plannedToolCall.binding.config.name,
|
|
@@ -262,6 +286,8 @@ export class AgentToolExecutionCoordinator {
|
|
|
262
286
|
queuedAt: args.startedAt.toISOString(),
|
|
263
287
|
startedAt: args.startedAt.toISOString(),
|
|
264
288
|
finishedAt: finishedAt.toISOString(),
|
|
289
|
+
iterationId: args.ctx.iterationId,
|
|
290
|
+
parentInvocationId: args.ctx.parentInvocationId,
|
|
265
291
|
});
|
|
266
292
|
}
|
|
267
293
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AssertionResult, RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
|
|
2
|
+
import { node } from "@codemation/core";
|
|
3
|
+
|
|
4
|
+
import { Assertion } from "./assertion";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs the author's `assertions` callback for each input item and emits one workflow `Item` per
|
|
8
|
+
* returned {@link AssertionResult} on `main`. Persistence is handled by a host-side subscriber
|
|
9
|
+
* to `nodeCompleted` events that filters on `config.emitsAssertions === true`; this node does
|
|
10
|
+
* not write to any store on its own.
|
|
11
|
+
*
|
|
12
|
+
* If the author callback throws, we emit a single synthetic AssertionResult with `errored: true`
|
|
13
|
+
* and `score: 0`. Without this catch the whole node would fail and no assertion row would be
|
|
14
|
+
* persisted — making the rollup blind to "the assertion code itself is broken." The synthetic
|
|
15
|
+
* row keeps `failedAssertionsByRunId` consistent and gives the UI something to surface.
|
|
16
|
+
*/
|
|
17
|
+
@node({ packageName: "@codemation/core-nodes" })
|
|
18
|
+
export class AssertionNode implements RunnableNode<Assertion<any>> {
|
|
19
|
+
kind = "node" as const;
|
|
20
|
+
outputPorts = ["main"] as const;
|
|
21
|
+
|
|
22
|
+
async execute(args: RunnableNodeExecuteArgs<Assertion<any>>): Promise<unknown> {
|
|
23
|
+
const ctx = args.ctx;
|
|
24
|
+
const config = ctx.config;
|
|
25
|
+
try {
|
|
26
|
+
const results: ReadonlyArray<AssertionResult> = await config.assertions(args.item, ctx);
|
|
27
|
+
// Engine "array → fan-out on main, each element is item.json" — returning the plain results
|
|
28
|
+
// makes downstream `item.json` exactly an AssertionResult. Wrapping in `{ json: result }`
|
|
29
|
+
// would double-wrap (engine would see `Item`-shaped values but treat them as JSON values).
|
|
30
|
+
return [...results];
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
const erroredResult: AssertionResult = {
|
|
34
|
+
name: config.name ?? "assertion",
|
|
35
|
+
score: 0,
|
|
36
|
+
errored: true,
|
|
37
|
+
message,
|
|
38
|
+
};
|
|
39
|
+
return [erroredResult];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TriggerNodeConfig, TypeToken } from "@codemation/core";
|
|
2
|
+
|
|
3
|
+
import { Cron } from "croner";
|
|
4
|
+
import type { CronCallback } from "croner";
|
|
5
|
+
|
|
6
|
+
import { CronTriggerNode } from "./CronTriggerNode";
|
|
7
|
+
|
|
8
|
+
export type CronTickJson = { firedAt: string; scheduledFor: string };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Schedules a workflow on a standard cron expression.
|
|
12
|
+
*
|
|
13
|
+
* Each tick emits one item: `{ firedAt: string, scheduledFor: string }` — both ISO-8601 timestamps.
|
|
14
|
+
* `firedAt` is the wall-clock moment the callback ran; `scheduledFor` is the cron-computed
|
|
15
|
+
* firing instant (these differ when the job was delayed).
|
|
16
|
+
*
|
|
17
|
+
* Timezone defaults to UTC when omitted — cron without an explicit TZ is a DST footgun.
|
|
18
|
+
*/
|
|
19
|
+
export class CronTrigger implements TriggerNodeConfig<CronTickJson> {
|
|
20
|
+
readonly kind = "trigger" as const;
|
|
21
|
+
readonly type: TypeToken<unknown> = CronTriggerNode;
|
|
22
|
+
readonly icon = "lucide:clock" as const;
|
|
23
|
+
readonly id?: string;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
public readonly name: string,
|
|
27
|
+
private readonly args: Readonly<{ schedule: string; timezone?: string }>,
|
|
28
|
+
id?: string,
|
|
29
|
+
) {
|
|
30
|
+
new Cron(args.schedule, { paused: true, timezone: args.timezone });
|
|
31
|
+
this.id = id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get schedule(): string {
|
|
35
|
+
return this.args.schedule;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get timezone(): string | undefined {
|
|
39
|
+
return this.args.timezone;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
createJob(callback: CronCallback): Cron {
|
|
43
|
+
return new Cron(this.args.schedule, { timezone: this.args.timezone, protect: true }, callback);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Items,
|
|
3
|
+
NodeExecutionContext,
|
|
4
|
+
NodeOutputs,
|
|
5
|
+
TestableTriggerNode,
|
|
6
|
+
TriggerSetupContext,
|
|
7
|
+
TriggerTestItemsContext,
|
|
8
|
+
} from "@codemation/core";
|
|
9
|
+
|
|
10
|
+
import { node } from "@codemation/core";
|
|
11
|
+
|
|
12
|
+
import { CronTrigger } from "./CronTriggerFactory";
|
|
13
|
+
|
|
14
|
+
@node({ packageName: "@codemation/core-nodes" })
|
|
15
|
+
export class CronTriggerNode implements TestableTriggerNode<CronTrigger> {
|
|
16
|
+
readonly kind = "trigger" as const;
|
|
17
|
+
readonly outputPorts = ["main"] as const;
|
|
18
|
+
|
|
19
|
+
async setup(ctx: TriggerSetupContext<CronTrigger>): Promise<undefined> {
|
|
20
|
+
const job = ctx.config.createJob(async (self) => {
|
|
21
|
+
const scheduledFor = self.currentRun()?.toISOString() ?? ctx.now().toISOString();
|
|
22
|
+
await ctx.emit([{ json: { firedAt: ctx.now().toISOString(), scheduledFor } }]);
|
|
23
|
+
});
|
|
24
|
+
ctx.registerCleanup({
|
|
25
|
+
stop: () => {
|
|
26
|
+
job.stop();
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async execute(items: Items, _ctx: NodeExecutionContext<CronTrigger>): Promise<NodeOutputs> {
|
|
33
|
+
return { main: items };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getTestItems(ctx: TriggerTestItemsContext<CronTrigger>): Promise<Items> {
|
|
37
|
+
const nowIso = ctx.now().toISOString();
|
|
38
|
+
return [{ json: { firedAt: nowIso, scheduledFor: nowIso } }];
|
|
39
|
+
}
|
|
40
|
+
}
|