@codemation/core-nodes 0.10.1 → 0.12.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 +125 -0
- package/dist/index.cjs +273 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +212 -71
- package/dist/index.d.ts +213 -72
- package/dist/index.js +273 -105
- package/dist/index.js.map +1 -1
- package/dist/metadata.json +1 -1
- package/package.json +3 -2
- package/src/chatModels/CodemationChatModelConfig.ts +9 -21
- package/src/chatModels/CodemationChatModelFactory.ts +12 -9
- package/src/chatModels/OpenAIChatModelFactory.ts +3 -2
- package/src/http/HttpBodyBuilder.ts +9 -0
- package/src/http/httpRequest.types.ts +10 -1
- package/src/index.ts +1 -1
- package/src/nodes/AIAgentConfig.ts +28 -0
- package/src/nodes/AIAgentNode.ts +84 -17
- package/src/nodes/AgentBinaryContentFactory.ts +74 -0
- package/src/nodes/CallbackNodeFactory.ts +9 -6
- package/src/nodes/CronTriggerFactory.ts +6 -2
- package/src/nodes/DeferredMetaToolStrategy.ts +8 -2
- package/src/nodes/ManualTriggerFactory.ts +15 -11
- package/src/nodes/WebhookTriggerFactory.ts +9 -2
- package/src/nodes/aggregate.ts +9 -2
- package/src/nodes/assertion.ts +3 -0
- package/src/nodes/filter.ts +9 -2
- package/src/nodes/httpRequest.ts +7 -2
- package/src/nodes/if.ts +9 -2
- package/src/nodes/isTestRun.ts +6 -2
- package/src/nodes/mapData.ts +4 -2
- package/src/nodes/merge.ts +9 -2
- package/src/nodes/noOp.ts +9 -2
- package/src/nodes/nodeOptions.types.ts +12 -0
- package/src/nodes/split.ts +9 -2
- package/src/nodes/subWorkflow.ts +9 -2
- package/src/nodes/switch.ts +7 -1
- package/src/nodes/wait.ts +9 -2
- package/src/workflowAuthoring/WorkflowChatModelFactory.types.ts +8 -2
- package/src/chatModels/ManagedModelFetcher.ts +0 -23
package/dist/metadata.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/core-nodes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@ai-sdk/anthropic": "^3.0.85",
|
|
31
32
|
"@ai-sdk/openai": "^3.0.53",
|
|
32
33
|
"@ai-sdk/provider": "^3.0.8",
|
|
33
34
|
"ai": "^6.0.168",
|
|
34
35
|
"croner": "^10.0.1",
|
|
35
|
-
"@codemation/core": "0.
|
|
36
|
+
"@codemation/core": "0.14.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/node": "^25.3.5",
|
|
@@ -4,25 +4,14 @@ import type { CanvasIconName } from "../canvasIconName";
|
|
|
4
4
|
import { CodemationChatModelFactory } from "./CodemationChatModelFactory";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Complexity token sent to the managed LLM broker.
|
|
8
|
+
* The broker maps this to a concrete provider model and thinking effort.
|
|
9
|
+
* low = cheapest/fastest (short classification, simple extraction)
|
|
10
|
+
* medium = default for most extraction/agent work
|
|
11
|
+
* high = complex multi-step reasoning
|
|
12
|
+
* xhigh = hardest problems, most capable model
|
|
8
13
|
*/
|
|
9
|
-
export
|
|
10
|
-
id: string;
|
|
11
|
-
modelId: string;
|
|
12
|
-
displayName: string;
|
|
13
|
-
providerKey: string;
|
|
14
|
-
inputCostPerMTok: number;
|
|
15
|
-
outputCostPerMTok: number;
|
|
16
|
-
contextWindow: number;
|
|
17
|
-
tier: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Bifrost-namespaced model ID. Kept as `string` so runtime-fetched model IDs
|
|
22
|
-
* (from the CP allowlist) work without compile-time enumeration.
|
|
23
|
-
* Story C replaced the prior hardcoded union with this open type.
|
|
24
|
-
*/
|
|
25
|
-
export type CodemationManagedModel = string;
|
|
14
|
+
export type ManagedComplexity = "low" | "medium" | "high" | "xhigh";
|
|
26
15
|
|
|
27
16
|
export class CodemationChatModelConfig implements ChatModelConfig {
|
|
28
17
|
readonly type = CodemationChatModelFactory;
|
|
@@ -32,14 +21,13 @@ export class CodemationChatModelConfig implements ChatModelConfig {
|
|
|
32
21
|
|
|
33
22
|
constructor(
|
|
34
23
|
public readonly name: string,
|
|
35
|
-
public readonly
|
|
24
|
+
public readonly complexity: ManagedComplexity,
|
|
36
25
|
presentationIn?: AgentCanvasPresentation<CanvasIconName>,
|
|
37
26
|
public readonly options?: Readonly<{
|
|
38
|
-
temperature?: number;
|
|
39
27
|
maxTokens?: number;
|
|
40
28
|
}>,
|
|
41
29
|
) {
|
|
42
|
-
this.modelName =
|
|
30
|
+
this.modelName = complexity;
|
|
43
31
|
this.presentation = presentationIn ?? { icon: "lucide:bot", label: name };
|
|
44
32
|
}
|
|
45
33
|
|
|
@@ -1,14 +1,12 @@
|
|
|
1
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
|
-
|
|
6
4
|
import type { CodemationChatModelConfig } from "./CodemationChatModelConfig";
|
|
7
5
|
import { managedHmacFetchFactory } from "./ManagedHmacSignerFactory.types";
|
|
8
6
|
|
|
9
7
|
@chatModel({ packageName: "@codemation/core-nodes" })
|
|
10
8
|
export class CodemationChatModelFactory implements ChatModelFactory<CodemationChatModelConfig> {
|
|
11
|
-
create(
|
|
9
|
+
async create(
|
|
12
10
|
args: Readonly<{ config: CodemationChatModelConfig; ctx: NodeExecutionContext<any> }>,
|
|
13
11
|
): Promise<ChatLanguageModel> {
|
|
14
12
|
// D5: read at session-create time so unpairing or misconfiguration surfaces at workflow run, not boot.
|
|
@@ -27,18 +25,23 @@ export class CodemationChatModelFactory implements ChatModelFactory<CodemationCh
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
const hmacFetch = managedHmacFetchFactory(workspaceId, pairingSecret);
|
|
28
|
+
// Lazy import: pulls @ai-sdk/anthropic + the `ai` SDK (~28MB RSS) only when a
|
|
29
|
+
// chat model is actually built. Non-AI workflows never load it.
|
|
30
|
+
// Using the Anthropic-native route so the broker's injected `thinking` /
|
|
31
|
+
// `output_config.effort` fields survive (they are stripped by the OpenAI-compat route).
|
|
32
|
+
const { createAnthropic } = await import("@ai-sdk/anthropic");
|
|
30
33
|
// apiKey is required by the AI SDK but unused — authentication is handled by the HMAC-signed fetch wrapper.
|
|
31
|
-
|
|
32
|
-
const
|
|
34
|
+
// baseURL: the SDK appends /messages → hits the broker's /v1/messages Anthropic-native route.
|
|
35
|
+
const provider = createAnthropic({ baseURL: `${gatewayUrl}/v1`, apiKey: "codemation-managed", fetch: hmacFetch });
|
|
36
|
+
const languageModel = provider(args.config.complexity);
|
|
33
37
|
|
|
34
|
-
return
|
|
38
|
+
return {
|
|
35
39
|
languageModel,
|
|
36
|
-
modelName: args.config.
|
|
40
|
+
modelName: args.config.complexity,
|
|
37
41
|
provider: "codemation-managed",
|
|
38
42
|
defaultCallOptions: {
|
|
39
43
|
maxOutputTokens: args.config.options?.maxTokens,
|
|
40
|
-
temperature: args.config.options?.temperature,
|
|
41
44
|
},
|
|
42
|
-
}
|
|
45
|
+
};
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
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
|
-
|
|
6
4
|
import type { OpenAiCredentialSession } from "./OpenAiCredentialSession";
|
|
7
5
|
import type { OpenAIChatModelConfig } from "./openAiChatModelConfig";
|
|
8
6
|
|
|
@@ -12,6 +10,9 @@ export class OpenAIChatModelFactory implements ChatModelFactory<OpenAIChatModelC
|
|
|
12
10
|
args: Readonly<{ config: OpenAIChatModelConfig; ctx: NodeExecutionContext<any> }>,
|
|
13
11
|
): Promise<ChatLanguageModel> {
|
|
14
12
|
const session = await args.ctx.getCredential<OpenAiCredentialSession>(args.config.credentialSlotKey);
|
|
13
|
+
// Lazy import: pulls @ai-sdk/openai + the `ai` SDK (~28MB RSS) only when a
|
|
14
|
+
// chat model is actually built. Non-AI workflows never load it.
|
|
15
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
15
16
|
const provider = createOpenAI({
|
|
16
17
|
apiKey: session.apiKey,
|
|
17
18
|
baseURL: session.baseUrl,
|
|
@@ -28,6 +28,15 @@ export class HttpBodyBuilder {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if (spec.kind === "json") {
|
|
31
|
+
// Backstop for callers that reach this with a string despite the `data: object`
|
|
32
|
+
// type (e.g. via `unknown`/`as`, or a resolved expression): re-stringifying it
|
|
33
|
+
// would double-encode into `"\"...\""`. Fail loud instead of shipping bad JSON.
|
|
34
|
+
if (typeof spec.data === "string") {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'HttpRequest body kind:"json" expects a serializable object for "data", but received a string. ' +
|
|
37
|
+
"Pass the object directly (e.g. { a: 1 }), not a pre-stringified JSON string (JSON.stringify(...)).",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
31
40
|
return {
|
|
32
41
|
body: JSON.stringify(spec.data),
|
|
33
42
|
contentType: "application/json",
|
|
@@ -11,7 +11,16 @@ export type BinaryRef = string;
|
|
|
11
11
|
*/
|
|
12
12
|
export type HttpBodySpec =
|
|
13
13
|
| Readonly<{ kind: "none" }>
|
|
14
|
-
| Readonly<{
|
|
14
|
+
| Readonly<{
|
|
15
|
+
kind: "json";
|
|
16
|
+
/**
|
|
17
|
+
* Serializable object/array to encode as the JSON body. Encoded exactly once via
|
|
18
|
+
* `JSON.stringify`, so pass the value directly (e.g. `{ a: 1 }`) — never a
|
|
19
|
+
* pre-stringified string (`JSON.stringify(...)`), which would double-encode it.
|
|
20
|
+
* Typed as `object` so a bare string/primitive is a compile-time error.
|
|
21
|
+
*/
|
|
22
|
+
data: object;
|
|
23
|
+
}>
|
|
15
24
|
| Readonly<{ kind: "form"; data: Readonly<Record<string, string>> }>
|
|
16
25
|
| Readonly<{
|
|
17
26
|
kind: "multipart";
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,6 @@ export * from "./chatModels/openAiChatModelConfig";
|
|
|
10
10
|
export * from "./chatModels/OpenAiChatModelPresetsFactory";
|
|
11
11
|
export * from "./chatModels/CodemationChatModelFactory";
|
|
12
12
|
export * from "./chatModels/CodemationChatModelConfig";
|
|
13
|
-
export * from "./chatModels/ManagedModelFetcher";
|
|
14
13
|
export * from "./nodes/aiAgent";
|
|
15
14
|
export * from "./nodes/assertion";
|
|
16
15
|
export * from "./nodes/CallbackNodeFactory";
|
|
@@ -26,6 +25,7 @@ export * from "./nodes/CronTriggerNode";
|
|
|
26
25
|
export * from "./nodes/ManualTriggerFactory";
|
|
27
26
|
export * from "./nodes/mapData";
|
|
28
27
|
export * from "./nodes/merge";
|
|
28
|
+
export * from "./nodes/nodeOptions.types";
|
|
29
29
|
export * from "./nodes/noOp";
|
|
30
30
|
export * from "./nodes/subWorkflow";
|
|
31
31
|
export * from "./nodes/testTrigger";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RetryPolicy,
|
|
3
3
|
type AgentGuardrailConfig,
|
|
4
|
+
type AgentMessageBuildArgs,
|
|
4
5
|
type AgentMessageConfig,
|
|
5
6
|
type AgentNodeConfig,
|
|
7
|
+
type BinaryAttachment,
|
|
6
8
|
type ChatModelConfig,
|
|
7
9
|
type NodeInspectorSummaryRow,
|
|
8
10
|
type RetryPolicySpec,
|
|
@@ -20,6 +22,7 @@ export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown> {
|
|
|
20
22
|
readonly chatModel: ChatModelConfig;
|
|
21
23
|
readonly tools?: ReadonlyArray<ToolConfig>;
|
|
22
24
|
readonly id?: string;
|
|
25
|
+
readonly description?: string;
|
|
23
26
|
readonly retryPolicy?: RetryPolicySpec;
|
|
24
27
|
readonly guardrails?: AgentGuardrailConfig;
|
|
25
28
|
/** Engine applies with {@link RunnableNodeConfig.inputSchema} before {@link AIAgentNode.execute}. */
|
|
@@ -49,6 +52,23 @@ export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown> {
|
|
|
49
52
|
* Defaults to `["gmail", "ocr", "webhook"]` when unset.
|
|
50
53
|
*/
|
|
51
54
|
readonly untrustedSources?: ReadonlyArray<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Whether file binaries are automatically passed to the chat model as native inline
|
|
57
|
+
* multimodal blocks. Defaults to `true`. Set to `false` to skip the binary-passdown step
|
|
58
|
+
* entirely (the node then behaves as if no binaries were present).
|
|
59
|
+
*/
|
|
60
|
+
readonly passBinariesToModel?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Explicit binaries to pass to the chat model, instead of the ones on the current item.
|
|
63
|
+
* Either a static array or a function resolved per item (so an author can forward binaries
|
|
64
|
+
* produced by an earlier node further back in the workflow). When provided, these replace
|
|
65
|
+
* `item.binary` as the passdown source. Ignored when {@link passBinariesToModel} is `false`.
|
|
66
|
+
* Every binary is passed (images as image blocks, all other types as file blocks); the
|
|
67
|
+
* provider surfaces an error at runtime if it doesn't support a given file type.
|
|
68
|
+
*/
|
|
69
|
+
readonly binaries?:
|
|
70
|
+
| ReadonlyArray<BinaryAttachment>
|
|
71
|
+
| ((args: AgentMessageBuildArgs<TInputJson>) => ReadonlyArray<BinaryAttachment>);
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
/**
|
|
@@ -67,6 +87,7 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
|
|
|
67
87
|
readonly chatModel: ChatModelConfig;
|
|
68
88
|
readonly tools: ReadonlyArray<ToolConfig>;
|
|
69
89
|
readonly id?: string;
|
|
90
|
+
readonly description?: string;
|
|
70
91
|
readonly retryPolicy: RetryPolicySpec;
|
|
71
92
|
readonly guardrails?: AgentGuardrailConfig;
|
|
72
93
|
readonly inputSchema?: ZodType<TInputJson>;
|
|
@@ -74,6 +95,10 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
|
|
|
74
95
|
readonly mcpServers?: ReadonlyArray<string>;
|
|
75
96
|
readonly pinnedMcpTools?: readonly string[];
|
|
76
97
|
readonly untrustedSources?: ReadonlyArray<string>;
|
|
98
|
+
readonly passBinariesToModel?: boolean;
|
|
99
|
+
readonly binaries?:
|
|
100
|
+
| ReadonlyArray<BinaryAttachment>
|
|
101
|
+
| ((args: AgentMessageBuildArgs<TInputJson>) => ReadonlyArray<BinaryAttachment>);
|
|
77
102
|
|
|
78
103
|
constructor(options: AIAgentOptions<TInputJson, TOutputJson>) {
|
|
79
104
|
this.name = options.name;
|
|
@@ -81,6 +106,7 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
|
|
|
81
106
|
this.chatModel = options.chatModel;
|
|
82
107
|
this.tools = options.tools ?? [];
|
|
83
108
|
this.id = options.id;
|
|
109
|
+
this.description = options.description;
|
|
84
110
|
this.retryPolicy = options.retryPolicy ?? RetryPolicy.defaultForAiAgent;
|
|
85
111
|
this.guardrails = options.guardrails;
|
|
86
112
|
this.inputSchema = options.inputSchema;
|
|
@@ -88,6 +114,8 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
|
|
|
88
114
|
this.mcpServers = options.mcpServers;
|
|
89
115
|
this.pinnedMcpTools = options.pinnedMcpTools;
|
|
90
116
|
this.untrustedSources = options.untrustedSources;
|
|
117
|
+
this.passBinariesToModel = options.passBinariesToModel;
|
|
118
|
+
this.binaries = options.binaries;
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
|
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AgentGuardrailConfig,
|
|
3
3
|
AgentMessageDto,
|
|
4
4
|
AgentToolCall,
|
|
5
|
+
BinaryAttachment,
|
|
5
6
|
ChatLanguageModel,
|
|
6
7
|
ChatLanguageModelCallOptions,
|
|
7
8
|
ChatModelConfig,
|
|
@@ -36,7 +37,6 @@ import {
|
|
|
36
37
|
} from "@codemation/core";
|
|
37
38
|
|
|
38
39
|
import type { AssistantModelMessage, GenerateTextResult, LanguageModel, ModelMessage, ToolSet } from "ai";
|
|
39
|
-
import { Output, generateText, jsonSchema } from "ai";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* OUTPUT generic must extend AI SDK's `Output<OUTPUT, PARTIAL, ELEMENT>` which is parametric on
|
|
@@ -52,6 +52,7 @@ import { AIAgentExecutionHelpersFactory } from "./AIAgentExecutionHelpersFactory
|
|
|
52
52
|
import { AgentToolExecutionCoordinator } from "./AgentToolExecutionCoordinator";
|
|
53
53
|
import { ConnectionCredentialExecutionContextFactory } from "./ConnectionCredentialExecutionContextFactory";
|
|
54
54
|
import { AgentMessageFactory } from "./AgentMessageFactory";
|
|
55
|
+
import { AgentBinaryContentFactory, type ResolvedAgentBinary } from "./AgentBinaryContentFactory";
|
|
55
56
|
import { AgentOutputFactory } from "./AgentOutputFactory";
|
|
56
57
|
import { AgentStructuredOutputRunner } from "./AgentStructuredOutputRunner";
|
|
57
58
|
import { AgentToolCallPortMap } from "./AgentToolCallPortMapFactory";
|
|
@@ -110,6 +111,13 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
110
111
|
NodeExecutionContext<AIAgent<any, any>>,
|
|
111
112
|
Promise<PreparedAgentExecution>
|
|
112
113
|
>();
|
|
114
|
+
/**
|
|
115
|
+
* The `ai` SDK, loaded lazily in {@link execute} so the SDK (~28MB RSS) stays
|
|
116
|
+
* off the boot path — non-AI workflows never load it. Every path runs through
|
|
117
|
+
* `execute` → `ensureAiSdk` before any sync helper touches `this.aiSdk`.
|
|
118
|
+
*/
|
|
119
|
+
private aiSdk!: typeof import("ai");
|
|
120
|
+
private aiSdkPromise: Promise<typeof import("ai")> | null = null;
|
|
113
121
|
|
|
114
122
|
constructor(
|
|
115
123
|
@inject(CoreTokens.NodeResolver)
|
|
@@ -135,6 +143,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
135
143
|
|
|
136
144
|
async execute(args: RunnableNodeExecuteArgs<AIAgent<any, any>>): Promise<unknown> {
|
|
137
145
|
const { ctx } = args;
|
|
146
|
+
await this.ensureAiSdk();
|
|
138
147
|
|
|
139
148
|
// HITL resume branch (story 10): the engine re-activates us after a human decision.
|
|
140
149
|
if (ctx.resumeContext) {
|
|
@@ -147,6 +156,11 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
147
156
|
return resultItem.json;
|
|
148
157
|
}
|
|
149
158
|
|
|
159
|
+
/** Load the `ai` SDK once per node instance (cached promise guards concurrent items). */
|
|
160
|
+
private async ensureAiSdk(): Promise<void> {
|
|
161
|
+
this.aiSdk = await (this.aiSdkPromise ??= import("ai"));
|
|
162
|
+
}
|
|
163
|
+
|
|
150
164
|
/**
|
|
151
165
|
* Resume path: re-enters the agent loop after a HITL suspension.
|
|
152
166
|
* Reconstructs the conversation from the checkpoint, injects the human decision
|
|
@@ -330,7 +344,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
330
344
|
const { ctx } = prepared;
|
|
331
345
|
const itemInputsByPort = AgentItemPortMap.fromItem(item);
|
|
332
346
|
const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, itemIndex, items);
|
|
333
|
-
const conversation: ModelMessage[] = [...this.createPromptMessages(item, itemIndex, items, ctx)];
|
|
347
|
+
const conversation: ModelMessage[] = [...(await this.createPromptMessages(item, itemIndex, items, ctx))];
|
|
334
348
|
if (ctx.config.outputSchema && itemScopedTools.length === 0) {
|
|
335
349
|
const structuredOutput = await this.structuredOutputRunner.resolve({
|
|
336
350
|
model: prepared.model,
|
|
@@ -596,15 +610,18 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
596
610
|
}
|
|
597
611
|
|
|
598
612
|
/**
|
|
599
|
-
*
|
|
600
|
-
*
|
|
613
|
+
* Resolves the HITL behavior for a tool binding, or `undefined` when it is not a HITL tool.
|
|
614
|
+
* A binding is HITL if either the backing node carries a `defineHumanApprovalNode` marker or the
|
|
615
|
+
* binding sets a per-binding `onRejected` via `asTool(..., { onRejected })`. The per-binding value
|
|
616
|
+
* wins over the node marker, so two tools backed by the same node can reject differently.
|
|
601
617
|
*/
|
|
602
618
|
private resolveHumanApprovalBehavior(config: ToolConfig): Readonly<{ onRejected: "halt" | "return" }> | undefined {
|
|
603
619
|
if (!this.isNodeBackedToolConfig(config)) return undefined;
|
|
604
620
|
const nodeConfig = config.node as unknown as { humanApprovalToolBehavior?: { onRejected?: "halt" | "return" } };
|
|
605
621
|
const marker = nodeConfig.humanApprovalToolBehavior;
|
|
606
|
-
|
|
607
|
-
|
|
622
|
+
const perBinding = config.onRejected;
|
|
623
|
+
if (marker === undefined && perBinding === undefined) return undefined;
|
|
624
|
+
return { onRejected: perBinding ?? marker?.onRejected ?? "return" };
|
|
608
625
|
}
|
|
609
626
|
|
|
610
627
|
/**
|
|
@@ -650,7 +667,8 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
650
667
|
*/
|
|
651
668
|
private buildToolSetFromResolved(resolvedTools: ReadonlyArray<ResolvedTool>): ToolSet {
|
|
652
669
|
if (resolvedTools.length === 0) return {};
|
|
653
|
-
const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof jsonSchema> }> =
|
|
670
|
+
const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
|
|
671
|
+
{};
|
|
654
672
|
for (const entry of resolvedTools) {
|
|
655
673
|
const schemaRecord = this.executionHelpers.createJsonSchemaRecord(entry.runtime.inputSchema, {
|
|
656
674
|
schemaName: entry.config.name,
|
|
@@ -661,7 +679,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
661
679
|
const description = isHitl ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : baseDescription;
|
|
662
680
|
toolSet[entry.config.name] = {
|
|
663
681
|
description,
|
|
664
|
-
inputSchema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]),
|
|
682
|
+
inputSchema: this.aiSdk.jsonSchema(schemaRecord as Parameters<typeof import("ai").jsonSchema>[0]),
|
|
665
683
|
};
|
|
666
684
|
}
|
|
667
685
|
return toolSet as unknown as ToolSet;
|
|
@@ -683,7 +701,8 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
683
701
|
*/
|
|
684
702
|
private buildToolSet(itemScopedTools: ReadonlyArray<ItemScopedToolBinding>): ToolSet | undefined {
|
|
685
703
|
if (itemScopedTools.length === 0) return undefined;
|
|
686
|
-
const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof jsonSchema> }> =
|
|
704
|
+
const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
|
|
705
|
+
{};
|
|
687
706
|
for (const entry of itemScopedTools) {
|
|
688
707
|
const schemaRecord = this.executionHelpers.createJsonSchemaRecord(entry.inputSchema, {
|
|
689
708
|
schemaName: entry.config.name,
|
|
@@ -698,7 +717,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
698
717
|
: baseDescription;
|
|
699
718
|
toolSet[entry.config.name] = {
|
|
700
719
|
description,
|
|
701
|
-
inputSchema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]),
|
|
720
|
+
inputSchema: this.aiSdk.jsonSchema(schemaRecord as Parameters<typeof import("ai").jsonSchema>[0]),
|
|
702
721
|
};
|
|
703
722
|
}
|
|
704
723
|
return toolSet as unknown as ToolSet;
|
|
@@ -756,7 +775,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
756
775
|
});
|
|
757
776
|
try {
|
|
758
777
|
const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
|
|
759
|
-
const result = await generateText({
|
|
778
|
+
const result = await this.aiSdk.generateText({
|
|
760
779
|
model: model.languageModel as LanguageModel,
|
|
761
780
|
messages: [...messages],
|
|
762
781
|
tools,
|
|
@@ -878,10 +897,10 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
878
897
|
requireObjectRoot: true,
|
|
879
898
|
})
|
|
880
899
|
: schema;
|
|
881
|
-
const outputSchema = Output.object({
|
|
882
|
-
schema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]) as never,
|
|
900
|
+
const outputSchema = this.aiSdk.Output.object({
|
|
901
|
+
schema: this.aiSdk.jsonSchema(schemaRecord as Parameters<typeof import("ai").jsonSchema>[0]) as never,
|
|
883
902
|
});
|
|
884
|
-
const result = await generateText({
|
|
903
|
+
const result = await this.aiSdk.generateText({
|
|
885
904
|
model: model.languageModel as LanguageModel,
|
|
886
905
|
messages: [...messages],
|
|
887
906
|
experimental_output: outputSchema,
|
|
@@ -1204,12 +1223,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
1204
1223
|
return JSON.parse(json) as JsonValue;
|
|
1205
1224
|
}
|
|
1206
1225
|
|
|
1207
|
-
private createPromptMessages(
|
|
1226
|
+
private async createPromptMessages(
|
|
1208
1227
|
item: Item,
|
|
1209
1228
|
itemIndex: number,
|
|
1210
1229
|
items: Items,
|
|
1211
1230
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
1212
|
-
): ReadonlyArray<ModelMessage
|
|
1231
|
+
): Promise<ReadonlyArray<ModelMessage>> {
|
|
1213
1232
|
const messages = AgentMessageConfigNormalizer.resolveFromInputOrConfig(item.json, ctx.config, {
|
|
1214
1233
|
item,
|
|
1215
1234
|
itemIndex,
|
|
@@ -1217,7 +1236,55 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
1217
1236
|
ctx,
|
|
1218
1237
|
});
|
|
1219
1238
|
const wrapped = this.wrapUntrustedSourceMessages(messages, item, ctx.config);
|
|
1220
|
-
|
|
1239
|
+
const promptMessages = AgentMessageFactory.createPromptMessages(wrapped);
|
|
1240
|
+
// Skip the passdown step entirely when the author opted out (default is on).
|
|
1241
|
+
if (ctx.config.passBinariesToModel === false) return promptMessages;
|
|
1242
|
+
const attachments = this.selectBinaryAttachments(item, itemIndex, items, ctx);
|
|
1243
|
+
const binaries = await this.resolveInlineBinaries(attachments, ctx);
|
|
1244
|
+
return AgentBinaryContentFactory.withBinaries(promptMessages, binaries);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Picks which attachments feed the passdown. When the author supplies `config.binaries`
|
|
1249
|
+
* (a static array or a per-item function — e.g. to forward binaries from an earlier node),
|
|
1250
|
+
* those replace the current item's attachments; otherwise the current item's `item.binary`
|
|
1251
|
+
* is used.
|
|
1252
|
+
*/
|
|
1253
|
+
private selectBinaryAttachments(
|
|
1254
|
+
item: Item,
|
|
1255
|
+
itemIndex: number,
|
|
1256
|
+
items: Items,
|
|
1257
|
+
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
1258
|
+
): ReadonlyArray<BinaryAttachment> {
|
|
1259
|
+
const manual = ctx.config.binaries;
|
|
1260
|
+
if (manual !== undefined) {
|
|
1261
|
+
return typeof manual === "function" ? manual({ item, itemIndex, items, ctx }) : manual;
|
|
1262
|
+
}
|
|
1263
|
+
return item.binary ? Object.values(item.binary) : [];
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Reads every attachment through `ctx.binary` (storage-backed, by reference — never base64 on
|
|
1268
|
+
* `item.json`) and resolves it to inline base64 so the agent can pass it to the chat model as a
|
|
1269
|
+
* native multimodal block. Images become image blocks; every other type (PDF, office docs, CSV,
|
|
1270
|
+
* JSON, …) becomes a file block — we don't filter by media type, so any binary can be fed to the
|
|
1271
|
+
* model. If the provider rejects an unsupported type the error surfaces at runtime, and the
|
|
1272
|
+
* workflow can filter the binary upstream.
|
|
1273
|
+
*/
|
|
1274
|
+
private async resolveInlineBinaries(
|
|
1275
|
+
attachments: ReadonlyArray<BinaryAttachment>,
|
|
1276
|
+
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
1277
|
+
): Promise<ReadonlyArray<ResolvedAgentBinary>> {
|
|
1278
|
+
const resolved: ResolvedAgentBinary[] = [];
|
|
1279
|
+
for (const attachment of attachments) {
|
|
1280
|
+
const bytes = await ctx.binary.getBytes(attachment);
|
|
1281
|
+
resolved.push({
|
|
1282
|
+
mediaType: attachment.mimeType,
|
|
1283
|
+
base64: Buffer.from(bytes).toString("base64"),
|
|
1284
|
+
...(attachment.filename ? { filename: attachment.filename } : {}),
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
return resolved;
|
|
1221
1288
|
}
|
|
1222
1289
|
|
|
1223
1290
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { FilePart, ImagePart, ModelMessage, TextPart, UserModelMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
/** A binary attachment already resolved to inline bytes, ready to become an AI SDK content part. */
|
|
4
|
+
export type ResolvedAgentBinary = Readonly<{
|
|
5
|
+
mediaType: string;
|
|
6
|
+
/** Base64-encoded bytes of the attachment. */
|
|
7
|
+
base64: string;
|
|
8
|
+
filename?: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Turns resolved file binaries into native AI SDK multimodal content parts and merges them into the
|
|
13
|
+
* agent prompt. Images (`image/*`) become {@link ImagePart}s; every other type (PDFs, office docs,
|
|
14
|
+
* CSV, JSON, …) becomes a {@link FilePart}. The provider maps these to its wire-level `image` /
|
|
15
|
+
* `document` blocks; an unsupported file type surfaces as a provider error at runtime.
|
|
16
|
+
*
|
|
17
|
+
* Parts are appended to the LAST user message so the binary travels alongside the author's prompt
|
|
18
|
+
* text (preserving any untrusted-source preamble that already wrapped that text). When no user
|
|
19
|
+
* message exists, a new user message carrying only the binaries is appended.
|
|
20
|
+
*/
|
|
21
|
+
export class AgentBinaryContentFactory {
|
|
22
|
+
static toContentPart(binary: ResolvedAgentBinary): ImagePart | FilePart {
|
|
23
|
+
if (binary.mediaType.startsWith("image/")) {
|
|
24
|
+
return { type: "image", image: binary.base64, mediaType: binary.mediaType };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
type: "file",
|
|
28
|
+
data: binary.base64,
|
|
29
|
+
mediaType: binary.mediaType,
|
|
30
|
+
...(binary.filename ? { filename: binary.filename } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static withBinaries(
|
|
35
|
+
messages: ReadonlyArray<ModelMessage>,
|
|
36
|
+
binaries: ReadonlyArray<ResolvedAgentBinary>,
|
|
37
|
+
): ReadonlyArray<ModelMessage> {
|
|
38
|
+
if (binaries.length === 0) return messages;
|
|
39
|
+
const parts = binaries.map((binary) => AgentBinaryContentFactory.toContentPart(binary));
|
|
40
|
+
|
|
41
|
+
const lastUserIndex = AgentBinaryContentFactory.lastUserMessageIndex(messages);
|
|
42
|
+
if (lastUserIndex === -1) {
|
|
43
|
+
const appended: UserModelMessage = { role: "user", content: parts };
|
|
44
|
+
return [...messages, appended];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const next = [...messages];
|
|
48
|
+
next[lastUserIndex] = AgentBinaryContentFactory.appendPartsToUserMessage(
|
|
49
|
+
messages[lastUserIndex] as UserModelMessage,
|
|
50
|
+
parts,
|
|
51
|
+
);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private static lastUserMessageIndex(messages: ReadonlyArray<ModelMessage>): number {
|
|
56
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
57
|
+
if (messages[index]?.role === "user") return index;
|
|
58
|
+
}
|
|
59
|
+
return -1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static appendPartsToUserMessage(
|
|
63
|
+
message: UserModelMessage,
|
|
64
|
+
parts: ReadonlyArray<ImagePart | FilePart>,
|
|
65
|
+
): UserModelMessage {
|
|
66
|
+
const existing: ReadonlyArray<TextPart | ImagePart | FilePart> =
|
|
67
|
+
typeof message.content === "string"
|
|
68
|
+
? message.content.length > 0
|
|
69
|
+
? [{ type: "text", text: message.content }]
|
|
70
|
+
: []
|
|
71
|
+
: message.content;
|
|
72
|
+
return { ...message, content: [...existing, ...parts] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "@codemation/core";
|
|
11
11
|
|
|
12
12
|
import { CallbackNode } from "./CallbackNode";
|
|
13
|
+
import type { NodeBaseOptions } from "./nodeOptions.types";
|
|
13
14
|
|
|
14
15
|
export type CallbackHandler<
|
|
15
16
|
TInputJson = unknown,
|
|
@@ -20,12 +21,12 @@ export type CallbackHandler<
|
|
|
20
21
|
ctx: NodeExecutionContext<TConfig>,
|
|
21
22
|
) => Promise<Items<TOutputJson> | PortsEmission | void> | Items<TOutputJson> | PortsEmission | void;
|
|
22
23
|
|
|
23
|
-
export type CallbackOptions =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}>;
|
|
24
|
+
export type CallbackOptions = NodeBaseOptions &
|
|
25
|
+
Readonly<{
|
|
26
|
+
retryPolicy?: RetryPolicySpec;
|
|
27
|
+
nodeErrorHandler?: NodeErrorHandlerSpec;
|
|
28
|
+
declaredOutputPorts?: ReadonlyArray<string>;
|
|
29
|
+
}>;
|
|
29
30
|
|
|
30
31
|
export class Callback<TInputJson = unknown, TOutputJson = TInputJson> implements RunnableNodeConfig<
|
|
31
32
|
TInputJson,
|
|
@@ -37,6 +38,7 @@ export class Callback<TInputJson = unknown, TOutputJson = TInputJson> implements
|
|
|
37
38
|
readonly icon = "lucide:braces" as const;
|
|
38
39
|
readonly emptyBatchExecution = "runOnce" as const;
|
|
39
40
|
readonly id?: string;
|
|
41
|
+
readonly description?: string;
|
|
40
42
|
readonly retryPolicy?: RetryPolicySpec;
|
|
41
43
|
readonly nodeErrorHandler?: NodeErrorHandlerSpec;
|
|
42
44
|
readonly declaredOutputPorts?: ReadonlyArray<string>;
|
|
@@ -52,6 +54,7 @@ export class Callback<TInputJson = unknown, TOutputJson = TInputJson> implements
|
|
|
52
54
|
) {
|
|
53
55
|
const resolvedOptions = typeof idOrOptions === "string" ? { ...options, id: idOrOptions } : idOrOptions;
|
|
54
56
|
this.id = resolvedOptions?.id;
|
|
57
|
+
this.description = resolvedOptions?.description;
|
|
55
58
|
this.retryPolicy = resolvedOptions?.retryPolicy;
|
|
56
59
|
this.nodeErrorHandler = resolvedOptions?.nodeErrorHandler;
|
|
57
60
|
this.declaredOutputPorts = resolvedOptions?.declaredOutputPorts;
|
|
@@ -4,6 +4,7 @@ import { Cron } from "croner";
|
|
|
4
4
|
import type { CronCallback } from "croner";
|
|
5
5
|
|
|
6
6
|
import { CronTriggerNode } from "./CronTriggerNode";
|
|
7
|
+
import type { NodeBaseOptions } from "./nodeOptions.types";
|
|
7
8
|
|
|
8
9
|
export type CronTickJson = { firedAt: string; scheduledFor: string };
|
|
9
10
|
|
|
@@ -21,14 +22,17 @@ export class CronTrigger implements TriggerNodeConfig<CronTickJson> {
|
|
|
21
22
|
readonly type: TypeToken<unknown> = CronTriggerNode;
|
|
22
23
|
readonly icon = "lucide:clock" as const;
|
|
23
24
|
readonly id?: string;
|
|
25
|
+
readonly description?: string;
|
|
24
26
|
|
|
25
27
|
constructor(
|
|
26
28
|
public readonly name: string,
|
|
27
29
|
private readonly args: Readonly<{ schedule: string; timezone?: string }>,
|
|
28
|
-
|
|
30
|
+
idOrOptions?: string | NodeBaseOptions,
|
|
29
31
|
) {
|
|
30
32
|
new Cron(args.schedule, { paused: true, timezone: args.timezone });
|
|
31
|
-
|
|
33
|
+
const options = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
|
|
34
|
+
this.id = options?.id;
|
|
35
|
+
this.description = options?.description;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
get schedule(): string {
|