@codemation/core-nodes 0.10.2 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/dist/index.cjs +259 -100
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +191 -66
  5. package/dist/index.d.ts +192 -67
  6. package/dist/index.js +259 -97
  7. package/dist/index.js.map +1 -1
  8. package/dist/metadata.json +1 -1
  9. package/package.json +3 -2
  10. package/src/chatModels/CodemationChatModelConfig.ts +9 -21
  11. package/src/chatModels/CodemationChatModelFactory.ts +12 -9
  12. package/src/chatModels/OpenAIChatModelFactory.ts +3 -2
  13. package/src/index.ts +1 -1
  14. package/src/nodes/AIAgentConfig.ts +28 -0
  15. package/src/nodes/AIAgentNode.ts +77 -13
  16. package/src/nodes/AgentBinaryContentFactory.ts +74 -0
  17. package/src/nodes/CallbackNodeFactory.ts +9 -6
  18. package/src/nodes/CronTriggerFactory.ts +6 -2
  19. package/src/nodes/DeferredMetaToolStrategy.ts +8 -2
  20. package/src/nodes/ManualTriggerFactory.ts +15 -11
  21. package/src/nodes/WebhookTriggerFactory.ts +9 -2
  22. package/src/nodes/aggregate.ts +9 -2
  23. package/src/nodes/assertion.ts +3 -0
  24. package/src/nodes/filter.ts +9 -2
  25. package/src/nodes/httpRequest.ts +6 -1
  26. package/src/nodes/if.ts +9 -2
  27. package/src/nodes/isTestRun.ts +6 -2
  28. package/src/nodes/mapData.ts +4 -2
  29. package/src/nodes/merge.ts +9 -2
  30. package/src/nodes/noOp.ts +9 -2
  31. package/src/nodes/nodeOptions.types.ts +12 -0
  32. package/src/nodes/split.ts +9 -2
  33. package/src/nodes/subWorkflow.ts +9 -2
  34. package/src/nodes/switch.ts +7 -1
  35. package/src/nodes/wait.ts +9 -2
  36. package/src/workflowAuthoring/WorkflowChatModelFactory.types.ts +8 -2
  37. package/src/chatModels/ManagedModelFetcher.ts +0 -23
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@codemation/core-nodes",
4
- "packageVersion": "0.10.2",
4
+ "packageVersion": "0.12.0",
5
5
  "description": "",
6
6
  "kind": "nodes",
7
7
  "nodes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/core-nodes",
3
- "version": "0.10.2",
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.13.2"
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
- * A platform-managed model entry as returned by GET /api/llm/managed-models.
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 interface ManagedModelDto {
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 model: CodemationManagedModel,
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 = model;
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
- const provider = createOpenAI({ baseURL: `${gatewayUrl}/v1`, apiKey: "codemation-managed", fetch: hmacFetch });
32
- const languageModel = provider.chat(args.config.model);
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 Promise.resolve({
38
+ return {
35
39
  languageModel,
36
- modelName: args.config.model,
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,
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> {
@@ -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,
@@ -653,7 +667,8 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
653
667
  */
654
668
  private buildToolSetFromResolved(resolvedTools: ReadonlyArray<ResolvedTool>): ToolSet {
655
669
  if (resolvedTools.length === 0) return {};
656
- const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof jsonSchema> }> = {};
670
+ const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
671
+ {};
657
672
  for (const entry of resolvedTools) {
658
673
  const schemaRecord = this.executionHelpers.createJsonSchemaRecord(entry.runtime.inputSchema, {
659
674
  schemaName: entry.config.name,
@@ -664,7 +679,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
664
679
  const description = isHitl ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : baseDescription;
665
680
  toolSet[entry.config.name] = {
666
681
  description,
667
- inputSchema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]),
682
+ inputSchema: this.aiSdk.jsonSchema(schemaRecord as Parameters<typeof import("ai").jsonSchema>[0]),
668
683
  };
669
684
  }
670
685
  return toolSet as unknown as ToolSet;
@@ -686,7 +701,8 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
686
701
  */
687
702
  private buildToolSet(itemScopedTools: ReadonlyArray<ItemScopedToolBinding>): ToolSet | undefined {
688
703
  if (itemScopedTools.length === 0) return undefined;
689
- const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof jsonSchema> }> = {};
704
+ const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
705
+ {};
690
706
  for (const entry of itemScopedTools) {
691
707
  const schemaRecord = this.executionHelpers.createJsonSchemaRecord(entry.inputSchema, {
692
708
  schemaName: entry.config.name,
@@ -701,7 +717,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
701
717
  : baseDescription;
702
718
  toolSet[entry.config.name] = {
703
719
  description,
704
- inputSchema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]),
720
+ inputSchema: this.aiSdk.jsonSchema(schemaRecord as Parameters<typeof import("ai").jsonSchema>[0]),
705
721
  };
706
722
  }
707
723
  return toolSet as unknown as ToolSet;
@@ -759,7 +775,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
759
775
  });
760
776
  try {
761
777
  const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
762
- const result = await generateText({
778
+ const result = await this.aiSdk.generateText({
763
779
  model: model.languageModel as LanguageModel,
764
780
  messages: [...messages],
765
781
  tools,
@@ -881,10 +897,10 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
881
897
  requireObjectRoot: true,
882
898
  })
883
899
  : schema;
884
- const outputSchema = Output.object({
885
- 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,
886
902
  });
887
- const result = await generateText({
903
+ const result = await this.aiSdk.generateText({
888
904
  model: model.languageModel as LanguageModel,
889
905
  messages: [...messages],
890
906
  experimental_output: outputSchema,
@@ -1207,12 +1223,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
1207
1223
  return JSON.parse(json) as JsonValue;
1208
1224
  }
1209
1225
 
1210
- private createPromptMessages(
1226
+ private async createPromptMessages(
1211
1227
  item: Item,
1212
1228
  itemIndex: number,
1213
1229
  items: Items,
1214
1230
  ctx: NodeExecutionContext<AIAgent<any, any>>,
1215
- ): ReadonlyArray<ModelMessage> {
1231
+ ): Promise<ReadonlyArray<ModelMessage>> {
1216
1232
  const messages = AgentMessageConfigNormalizer.resolveFromInputOrConfig(item.json, ctx.config, {
1217
1233
  item,
1218
1234
  itemIndex,
@@ -1220,7 +1236,55 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
1220
1236
  ctx,
1221
1237
  });
1222
1238
  const wrapped = this.wrapUntrustedSourceMessages(messages, item, ctx.config);
1223
- return AgentMessageFactory.createPromptMessages(wrapped);
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;
1224
1288
  }
1225
1289
 
1226
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 = Readonly<{
24
- id?: string;
25
- retryPolicy?: RetryPolicySpec;
26
- nodeErrorHandler?: NodeErrorHandlerSpec;
27
- declaredOutputPorts?: ReadonlyArray<string>;
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
- id?: string,
30
+ idOrOptions?: string | NodeBaseOptions,
29
31
  ) {
30
32
  new Cron(args.schedule, { paused: true, timezone: args.timezone });
31
- this.id = id;
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 {
@@ -1,5 +1,4 @@
1
1
  import type { ToolSet } from "ai";
2
- import { jsonSchema } from "ai";
3
2
  import { z } from "zod";
4
3
  import type { BM25Index } from "./BM25Index";
5
4
  import type {
@@ -37,6 +36,12 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
37
36
  private mcpEntries: McpToolEntry[] = [];
38
37
  private toolsByServerId = new Map<string, Map<string, ToolSet[string]>>();
39
38
  private foundToolIds = new Set<string>();
39
+ /**
40
+ * `jsonSchema` from the `ai` SDK, loaded lazily in {@link initialize} so the SDK
41
+ * (~28MB RSS) stays off the boot path. `initialize` always runs before the sync
42
+ * `getToolsForTurn` → `buildFindToolsDefinition` path, so this is set before use.
43
+ */
44
+ private jsonSchema!: typeof import("ai").jsonSchema;
40
45
 
41
46
  constructor(
42
47
  private readonly bm25: BM25Index,
@@ -44,6 +49,7 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
44
49
  ) {}
45
50
 
46
51
  async initialize(input: ToolLoadingStrategyInitInput): Promise<void> {
52
+ this.jsonSchema = (await import("ai")).jsonSchema;
47
53
  this.nodeBackedTools = { ...input.nodeBackedTools };
48
54
 
49
55
  const pinnedIds = input.pinnedMcpTools ?? [];
@@ -194,7 +200,7 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
194
200
  "After this call, the tools listed in the result will be callable on your very next turn. " +
195
201
  "Use this when you need a capability not visible in your current tool list. " +
196
202
  "Do not attempt to call a tool name you have not seen yet — use find_tools to discover it first.",
197
- inputSchema: jsonSchema(inputSchemaRecord),
203
+ inputSchema: this.jsonSchema(inputSchemaRecord),
198
204
  } as unknown as ToolSet[string];
199
205
  }
200
206
  }