@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 +17 -0
- package/dist/index.cjs +175 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +175 -9
- package/dist/index.js.map +1 -1
- package/dist/metadata.json +1 -1
- package/package.json +1 -1
- package/src/nodes/AIAgentConfig.ts +8 -0
- package/src/nodes/AIAgentNode.ts +4 -2
- package/src/nodes/AgentMessageFactory.ts +22 -6
- package/src/nodes/AgentToolResultContentFactory.ts +155 -0
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() {
|