@codemation/core-nodes 0.12.0 → 0.13.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @codemation/core-nodes
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#240](https://github.com/MadeRelevant/codemation/pull/240) [`d786fc6`](https://github.com/MadeRelevant/codemation/commit/d786fc6ff8684a9c29a6b343b64f2d813d745a5e) Thanks [@cblokland90](https://github.com/cblokland90)! - feat(core-nodes): agent nodes pass tool-returned image/PDF binaries to the chat model
8
+
9
+ When an agent tool (including MCP tools like Gmail/Drive) returns a binary — an image or
10
+ a PDF — inside a `CallToolResult` content array, the agent now forwards it to the chat model
11
+ as a native multimodal tool-result block instead of flattening it to inert JSON text. Images
12
+ become `image-data` parts (image blocks on the wire); PDFs become `file-data` document parts.
13
+ `resource_link`s, embedded text resources, and unsupported binary types surface as text
14
+ markers rather than being dropped, and a per-result size cap replaces oversize binaries with a
15
+ placeholder. This is the tool-result analogue of input-time `item.binary` passdown.
16
+
17
+ A new `passToolBinariesToModel` option on the AI agent (default `true`) keeps the old inert
18
+ JSON behavior when set to `false`.
19
+
3
20
  ## 0.12.0
4
21
 
5
22
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -4486,6 +4486,155 @@ var CodemationChatModelConfig = class {
4486
4486
  }
4487
4487
  };
4488
4488
 
4489
+ //#endregion
4490
+ //#region src/nodes/AgentToolResultContentFactory.ts
4491
+ /**
4492
+ * Cap on raw (pre-base64) bytes inlined from a single tool result. Base64 inflates ~33% and every
4493
+ * inlined byte eats model context, so oversize binaries are replaced with a text placeholder.
4494
+ */
4495
+ const MAX_INLINE_BYTES = 8 * 1024 * 1024;
4496
+ /** MCP content-block discriminators. At least one must appear for a result to be treated as MCP-shaped. */
4497
+ const KNOWN_MCP_BLOCK_TYPES = new Set([
4498
+ "text",
4499
+ "image",
4500
+ "audio",
4501
+ "resource",
4502
+ "resource_link"
4503
+ ]);
4504
+ /**
4505
+ * Maps a tool result that is **content-block-shaped** (an MCP `CallToolResult` with a `content`
4506
+ * array) into AI SDK `{ type: "content" }` tool-result output, so binaries reach the chat model as
4507
+ * native multimodal tool-result blocks instead of being flattened to inert JSON text.
4508
+ *
4509
+ * The `@ai-sdk/anthropic` provider maps a `content`-output part as follows:
4510
+ * `text` → text block, `image-data` → image block, `file-data` (only `application/pdf`) → document
4511
+ * block. Non-PDF `file-data` is dropped by the provider, so this factory emits `image-data` for
4512
+ * images, `file-data` only for PDFs, and a text marker for every other binary type.
4513
+ *
4514
+ * Returns `undefined` when the result is not content-block-shaped — callers keep the existing
4515
+ * `{ type: "json" }` path, so plain string/object tool results are unaffected.
4516
+ */
4517
+ var AgentToolResultContentFactory = class AgentToolResultContentFactory {
4518
+ static tryMapToContentOutput(result) {
4519
+ const blocks = AgentToolResultContentFactory.contentBlocks(result);
4520
+ if (blocks === void 0) return void 0;
4521
+ const parts = [];
4522
+ let inlinedBytes = 0;
4523
+ for (const block of blocks) {
4524
+ const mapped = AgentToolResultContentFactory.mapBlock(block, inlinedBytes);
4525
+ parts.push(mapped.part);
4526
+ inlinedBytes += mapped.bytes;
4527
+ }
4528
+ return parts;
4529
+ }
4530
+ /**
4531
+ * Returns the `content` array iff `result` is an object whose `content` is an array of typed
4532
+ * blocks AND at least one block carries a known MCP discriminator. A plain JSON result that merely
4533
+ * has a `content` key of some other shape (e.g. Notion/Slack rich-text blocks) is rejected,
4534
+ * preserving the `{ type: "json" }` path so its payload is never lost.
4535
+ */
4536
+ static contentBlocks(result) {
4537
+ if (result === null || typeof result !== "object") return void 0;
4538
+ const content = result.content;
4539
+ if (!Array.isArray(content) || content.length === 0) return void 0;
4540
+ if (!content.every((block) => block !== null && typeof block === "object" && typeof block.type === "string")) return void 0;
4541
+ return content.some((block) => KNOWN_MCP_BLOCK_TYPES.has(block.type)) ? content : void 0;
4542
+ }
4543
+ static mapBlock(block, inlinedBytesSoFar) {
4544
+ const type = block.type;
4545
+ if (type === "text" && typeof block.text === "string") return {
4546
+ part: {
4547
+ type: "text",
4548
+ text: block.text
4549
+ },
4550
+ bytes: 0
4551
+ };
4552
+ if (type === "image" && typeof block.data === "string" && typeof block.mimeType === "string") return AgentToolResultContentFactory.mapBinary({
4553
+ base64: block.data,
4554
+ mediaType: block.mimeType,
4555
+ inlinedBytesSoFar
4556
+ });
4557
+ if (type === "resource" && block.resource) return AgentToolResultContentFactory.mapEmbeddedResource(block.resource, inlinedBytesSoFar);
4558
+ if (type === "resource_link" && typeof block.uri === "string") {
4559
+ const mime = typeof block.mimeType === "string" ? ` (${block.mimeType})` : "";
4560
+ return {
4561
+ part: {
4562
+ type: "text",
4563
+ text: `[linked resource: ${block.uri}${mime}]`
4564
+ },
4565
+ bytes: 0
4566
+ };
4567
+ }
4568
+ return {
4569
+ part: {
4570
+ type: "text",
4571
+ text: `[unsupported tool content block: ${String(type)}]`
4572
+ },
4573
+ bytes: 0
4574
+ };
4575
+ }
4576
+ static mapEmbeddedResource(resource, inlinedBytesSoFar) {
4577
+ if (typeof resource.text === "string") return {
4578
+ part: {
4579
+ type: "text",
4580
+ text: resource.text
4581
+ },
4582
+ bytes: 0
4583
+ };
4584
+ if (typeof resource.blob === "string" && typeof resource.mimeType === "string") return AgentToolResultContentFactory.mapBinary({
4585
+ base64: resource.blob,
4586
+ mediaType: resource.mimeType,
4587
+ filename: typeof resource.name === "string" ? resource.name : void 0,
4588
+ inlinedBytesSoFar
4589
+ });
4590
+ return {
4591
+ part: {
4592
+ type: "text",
4593
+ text: `[embedded resource: ${typeof resource.uri === "string" ? resource.uri : "unknown"}]`
4594
+ },
4595
+ bytes: 0
4596
+ };
4597
+ }
4598
+ static mapBinary(args) {
4599
+ const rawBytes = Math.floor(args.base64.length * 3 / 4);
4600
+ if (args.inlinedBytesSoFar + rawBytes > MAX_INLINE_BYTES) {
4601
+ const name = args.filename ? ` "${args.filename}"` : "";
4602
+ const kb = Math.round(rawBytes / 1024);
4603
+ return {
4604
+ part: {
4605
+ type: "text",
4606
+ text: `[binary${name} (${args.mediaType}, ~${kb} KB) omitted: exceeds the per-tool-result inline limit]`
4607
+ },
4608
+ bytes: 0
4609
+ };
4610
+ }
4611
+ if (args.mediaType.startsWith("image/")) return {
4612
+ part: {
4613
+ type: "image-data",
4614
+ data: args.base64,
4615
+ mediaType: args.mediaType
4616
+ },
4617
+ bytes: rawBytes
4618
+ };
4619
+ if (args.mediaType === "application/pdf") return {
4620
+ part: {
4621
+ type: "file-data",
4622
+ data: args.base64,
4623
+ mediaType: args.mediaType,
4624
+ ...args.filename ? { filename: args.filename } : {}
4625
+ },
4626
+ bytes: rawBytes
4627
+ };
4628
+ return {
4629
+ part: {
4630
+ type: "text",
4631
+ text: `[binary${args.filename ? ` "${args.filename}"` : ""} (${args.mediaType}) not inlined: unsupported by the model]`
4632
+ },
4633
+ bytes: 0
4634
+ };
4635
+ }
4636
+ };
4637
+
4489
4638
  //#endregion
4490
4639
  //#region src/nodes/AgentMessageFactory.ts
4491
4640
  /**
@@ -4522,20 +4671,35 @@ var AgentMessageFactory = class AgentMessageFactory {
4522
4671
  * Builds the `{ role: "tool", content: [{ type: "tool-result", ... }, ...] }` message returned
4523
4672
  * to the model after each tool round.
4524
4673
  */
4525
- static createToolResultsMessage(executedToolCalls) {
4674
+ static createToolResultsMessage(executedToolCalls, passToolBinariesToModel = true) {
4526
4675
  return {
4527
4676
  role: "tool",
4528
4677
  content: executedToolCalls.map((executed) => ({
4529
4678
  type: "tool-result",
4530
4679
  toolCallId: executed.toolCallId,
4531
4680
  toolName: executed.toolName,
4532
- output: {
4533
- type: "json",
4534
- value: AgentMessageFactory.toToolResultJson(executed.result)
4535
- }
4681
+ output: AgentMessageFactory.toToolResultOutput(executed.result, passToolBinariesToModel)
4536
4682
  }))
4537
4683
  };
4538
4684
  }
4685
+ /**
4686
+ * Routes a tool result to a native multimodal `{ type: "content" }` output when it is
4687
+ * content-block-shaped (an MCP `CallToolResult`) and binary passdown is enabled; otherwise keeps
4688
+ * the inert `{ type: "json" }` path.
4689
+ */
4690
+ static toToolResultOutput(result, passToolBinariesToModel) {
4691
+ if (passToolBinariesToModel) {
4692
+ const content = AgentToolResultContentFactory.tryMapToContentOutput(result);
4693
+ if (content !== void 0) return {
4694
+ type: "content",
4695
+ value: content
4696
+ };
4697
+ }
4698
+ return {
4699
+ type: "json",
4700
+ value: AgentMessageFactory.toToolResultJson(result)
4701
+ };
4702
+ }
4539
4703
  static toToolResultJson(value) {
4540
4704
  if (value === void 0) return null;
4541
4705
  try {
@@ -6324,7 +6488,7 @@ let AIAgentNode = class AIAgentNode$1 {
6324
6488
  result: decision,
6325
6489
  serialized: JSON.stringify(decision)
6326
6490
  };
6327
- const conversation = [...checkpoint.conversation, AgentMessageFactory.createToolResultsMessage([toolResultEntry])];
6491
+ const conversation = [...checkpoint.conversation, AgentMessageFactory.createToolResultsMessage([toolResultEntry], ctx.config.passToolBinariesToModel !== false)];
6328
6492
  const loopResult = await this.runTurnLoopUntilFinalAnswer({
6329
6493
  prepared,
6330
6494
  itemInputsByPort,
@@ -6544,7 +6708,7 @@ let AIAgentNode = class AIAgentNode$1 {
6544
6708
  coordinatorExecutedCalls.push(...executed);
6545
6709
  }
6546
6710
  const allExecutedCalls = [...strategyExecutedCalls, ...coordinatorExecutedCalls];
6547
- this.appendAssistantAndToolMessages(conversation, result.assistantMessage, result.text, result.toolCalls, allExecutedCalls);
6711
+ this.appendAssistantAndToolMessages(conversation, result.assistantMessage, result.text, result.toolCalls, allExecutedCalls, ctx.config.passToolBinariesToModel !== false);
6548
6712
  }
6549
6713
  return {
6550
6714
  finalText,
@@ -6559,8 +6723,8 @@ let AIAgentNode = class AIAgentNode$1 {
6559
6723
  if (guardrails.onTurnLimitReached === "respondWithLastMessage") return;
6560
6724
  throw new Error(`AIAgent "${ctx.config.name ?? ctx.nodeId}" reached maxTurns=${guardrails.maxTurns} before producing a final response.`);
6561
6725
  }
6562
- appendAssistantAndToolMessages(conversation, assistantMessage, text, toolCalls, executedToolCalls) {
6563
- conversation.push(assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(text, toolCalls), AgentMessageFactory.createToolResultsMessage(executedToolCalls));
6726
+ appendAssistantAndToolMessages(conversation, assistantMessage, text, toolCalls, executedToolCalls, passToolBinariesToModel) {
6727
+ conversation.push(assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(text, toolCalls), AgentMessageFactory.createToolResultsMessage(executedToolCalls, passToolBinariesToModel));
6564
6728
  }
6565
6729
  async resolveFinalOutputJson(prepared, itemInputsByPort, conversation, finalText, wasToolEnabledRun) {
6566
6730
  if (!prepared.ctx.config.outputSchema) return AgentOutputFactory.fromAgentContent(finalText);
@@ -7324,6 +7488,7 @@ var AIAgent = class {
7324
7488
  pinnedMcpTools;
7325
7489
  untrustedSources;
7326
7490
  passBinariesToModel;
7491
+ passToolBinariesToModel;
7327
7492
  binaries;
7328
7493
  constructor(options) {
7329
7494
  this.name = options.name;
@@ -7340,6 +7505,7 @@ var AIAgent = class {
7340
7505
  this.pinnedMcpTools = options.pinnedMcpTools;
7341
7506
  this.untrustedSources = options.untrustedSources;
7342
7507
  this.passBinariesToModel = options.passBinariesToModel;
7508
+ this.passToolBinariesToModel = options.passToolBinariesToModel;
7343
7509
  this.binaries = options.binaries;
7344
7510
  }
7345
7511
  inspectorSummary() {