@codemation/core-nodes 0.12.0 → 0.14.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 +30 -0
- package/dist/index.cjs +180 -413
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -1303
- package/dist/index.d.ts +99 -1303
- package/dist/index.js +181 -415
- package/dist/index.js.map +1 -1
- package/dist/metadata.json +1 -1
- package/package.json +2 -2
- package/src/authoring/defineRestNode.types.ts +0 -84
- package/src/canvasIconName.ts +0 -7
- package/src/chatModels/CodemationChatModelConfig.ts +0 -10
- package/src/chatModels/CodemationChatModelFactory.ts +0 -7
- package/src/chatModels/ManagedHmacSignerFactory.types.ts +0 -35
- package/src/chatModels/OpenAIChatModelFactory.ts +0 -2
- package/src/chatModels/OpenAiChatModelPresetsFactory.ts +0 -5
- package/src/chatModels/OpenAiCredentialSession.ts +0 -1
- package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +0 -21
- package/src/credentials/ApiKeyCredentialType.ts +0 -3
- package/src/credentials/BasicAuthCredentialType.ts +0 -4
- package/src/credentials/BearerTokenCredentialType.ts +0 -4
- package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +0 -19
- package/src/credentials/OAuth2TokenExchangeFactory.ts +0 -7
- package/src/http/HttpBodyBuilder.ts +0 -16
- package/src/http/HttpRequestExecutor.ts +0 -35
- package/src/http/HttpUrlBuilder.ts +0 -4
- package/src/http/SSRFBlockedError.ts +0 -4
- package/src/http/SsrfGuard.ts +10 -50
- package/src/http/httpRequest.types.ts +0 -49
- package/src/index.ts +1 -0
- package/src/nodes/AIAgentConfig.ts +3 -39
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +0 -37
- package/src/nodes/AIAgentNode.ts +4 -134
- package/src/nodes/AgentBinaryContentFactory.ts +0 -12
- package/src/nodes/AgentLoopCheckpoint.types.ts +0 -13
- package/src/nodes/AgentMessageFactory.ts +17 -19
- package/src/nodes/AgentStructuredOutputRunner.ts +0 -17
- package/src/nodes/AgentToolExecutionCoordinator.ts +0 -12
- package/src/nodes/AgentToolResultContentFactory.ts +126 -0
- package/src/nodes/AssertionNode.ts +0 -14
- package/src/nodes/BM25Index.ts +0 -14
- package/src/nodes/ConnectionCredentialExecutionContextFactory.ts +0 -5
- package/src/nodes/ConnectionCredentialNode.ts +0 -4
- package/src/nodes/CronTriggerFactory.ts +0 -9
- package/src/nodes/DeferredMetaToolStrategy.ts +0 -18
- package/src/nodes/DeferredMetaToolStrategyFactory.ts +0 -5
- package/src/nodes/HttpRequestNodeFactory.ts +0 -14
- package/src/nodes/InboxApprovalNode.types.ts +0 -16
- package/src/nodes/IsTestRunNode.ts +0 -8
- package/src/nodes/ManualTriggerFactory.ts +0 -3
- package/src/nodes/ManualTriggerNode.ts +0 -4
- package/src/nodes/MergeNode.ts +0 -1
- package/src/nodes/NodeBackedToolRuntime.ts +0 -14
- package/src/nodes/SubWorkflowNode.ts +0 -3
- package/src/nodes/SwitchNode.ts +0 -3
- package/src/nodes/TestTriggerNode.ts +0 -9
- package/src/nodes/aiAgentSupport.types.ts +0 -16
- package/src/nodes/assertion.ts +0 -10
- package/src/nodes/codemationDocumentScannerNode.ts +0 -18
- package/src/nodes/collections/collectionListNode.types.ts +0 -1
- package/src/nodes/httpRequest.ts +0 -68
- package/src/nodes/isTestRun.ts +0 -4
- package/src/nodes/mapData.ts +0 -1
- package/src/nodes/merge.ts +0 -4
- package/src/nodes/mergeExecutionUtils.types.ts +0 -3
- package/src/nodes/nodeOptions.types.ts +0 -8
- package/src/nodes/schedulePollingTrigger.ts +37 -0
- package/src/nodes/split.ts +0 -4
- package/src/nodes/testTrigger.ts +0 -21
- package/src/nodes/wait.ts +0 -1
- package/src/nodes/webhookTriggerNode.ts +0 -5
- package/src/register.types.ts +0 -10
- package/src/workflows/AIAgentConnectionWorkflowExpander.ts +0 -3
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -38,11 +38,6 @@ import {
|
|
|
38
38
|
|
|
39
39
|
import type { AssistantModelMessage, GenerateTextResult, LanguageModel, ModelMessage, ToolSet } from "ai";
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* OUTPUT generic must extend AI SDK's `Output<OUTPUT, PARTIAL, ELEMENT>` which is parametric on
|
|
43
|
-
* `any`; there is no narrower concrete type we can substitute that accepts both text-only and
|
|
44
|
-
* structured turns uniformly.
|
|
45
|
-
*/
|
|
46
41
|
type AnyGenerateTextResult = GenerateTextResult<ToolSet, any>;
|
|
47
42
|
import { z } from "zod";
|
|
48
43
|
|
|
@@ -74,7 +69,6 @@ const HITL_SOLO_CONSTRAINT_SENTENCE =
|
|
|
74
69
|
type ResolvedGuardrails = Required<Pick<AgentGuardrailConfig, "maxTurns" | "onTurnLimitReached">> &
|
|
75
70
|
Pick<AgentGuardrailConfig, "modelInvocationOptions">;
|
|
76
71
|
|
|
77
|
-
/** Everything needed to run the agent loop for one item (shared across items in the same activation). */
|
|
78
72
|
interface PreparedAgentExecution {
|
|
79
73
|
readonly ctx: NodeExecutionContext<AIAgent<any, any>>;
|
|
80
74
|
readonly model: ChatLanguageModel;
|
|
@@ -84,7 +78,6 @@ interface PreparedAgentExecution {
|
|
|
84
78
|
readonly toolLoadingStrategy: ToolLoadingStrategy;
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
/** Result of one `generateText` turn with tools disabled for auto-execution. */
|
|
88
81
|
interface TurnResult {
|
|
89
82
|
readonly assistantMessage: AssistantModelMessage | undefined;
|
|
90
83
|
readonly text: string;
|
|
@@ -111,11 +104,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
111
104
|
NodeExecutionContext<AIAgent<any, any>>,
|
|
112
105
|
Promise<PreparedAgentExecution>
|
|
113
106
|
>();
|
|
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
107
|
private aiSdk!: typeof import("ai");
|
|
120
108
|
private aiSdkPromise: Promise<typeof import("ai")> | null = null;
|
|
121
109
|
|
|
@@ -145,7 +133,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
145
133
|
const { ctx } = args;
|
|
146
134
|
await this.ensureAiSdk();
|
|
147
135
|
|
|
148
|
-
// HITL resume branch (story 10): the engine re-activates us after a human decision.
|
|
149
136
|
if (ctx.resumeContext) {
|
|
150
137
|
return this.executeResumed(args, ctx.resumeContext);
|
|
151
138
|
}
|
|
@@ -156,16 +143,10 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
156
143
|
return resultItem.json;
|
|
157
144
|
}
|
|
158
145
|
|
|
159
|
-
/** Load the `ai` SDK once per node instance (cached promise guards concurrent items). */
|
|
160
146
|
private async ensureAiSdk(): Promise<void> {
|
|
161
147
|
this.aiSdk = await (this.aiSdkPromise ??= import("ai"));
|
|
162
148
|
}
|
|
163
149
|
|
|
164
|
-
/**
|
|
165
|
-
* Resume path: re-enters the agent loop after a HITL suspension.
|
|
166
|
-
* Reconstructs the conversation from the checkpoint, injects the human decision
|
|
167
|
-
* as a tool_result, and continues the loop from where it suspended.
|
|
168
|
-
*/
|
|
169
150
|
private async executeResumed(
|
|
170
151
|
args: RunnableNodeExecuteArgs<AIAgent<any, any>>,
|
|
171
152
|
resumeContext: ResumeContext,
|
|
@@ -176,14 +157,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
176
157
|
const onRejected = (taskMetadata["onRejected"] as "halt" | "return" | undefined) ?? "return";
|
|
177
158
|
|
|
178
159
|
if (!checkpoint) {
|
|
179
|
-
// Not an agent-HITL resume (e.g., a direct HITL node, not wrapped in agent). Fall through.
|
|
180
160
|
const prepared = await this.getOrPrepareExecution(ctx);
|
|
181
161
|
const itemWithMappedJson = { ...args.item, json: args.input };
|
|
182
162
|
const resultItem = await this.runAgentForItem(prepared, itemWithMappedJson, args.itemIndex, args.items);
|
|
183
163
|
return resultItem.json;
|
|
184
164
|
}
|
|
185
165
|
|
|
186
|
-
// If rejected with halt policy, the engine has already halted; return gracefully.
|
|
187
166
|
if (resumeContext.decision.kind === "decided" && resumeContext.decision.value === null && onRejected === "halt") {
|
|
188
167
|
return undefined;
|
|
189
168
|
}
|
|
@@ -191,7 +170,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
191
170
|
const decision = this.normalizeDecision(resumeContext);
|
|
192
171
|
|
|
193
172
|
if (decision.status === "rejected" && onRejected === "halt") {
|
|
194
|
-
// Engine halts the run. Return nothing — the run is dead.
|
|
195
173
|
return undefined;
|
|
196
174
|
}
|
|
197
175
|
|
|
@@ -200,8 +178,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
200
178
|
const itemInputsByPort = AgentItemPortMap.fromItem(item);
|
|
201
179
|
const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, args.itemIndex, args.items);
|
|
202
180
|
|
|
203
|
-
// Reconstruct conversation: checkpoint.conversation already includes the assistant message
|
|
204
|
-
// with the pending tool_use. Append the tool_result for the decision.
|
|
205
181
|
const toolResultEntry: ExecutedToolCall = {
|
|
206
182
|
toolName: checkpoint.pendingToolCallId,
|
|
207
183
|
toolCallId: checkpoint.pendingToolCallId,
|
|
@@ -210,7 +186,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
210
186
|
};
|
|
211
187
|
const conversation: ModelMessage[] = [
|
|
212
188
|
...checkpoint.conversation,
|
|
213
|
-
AgentMessageFactory.createToolResultsMessage([toolResultEntry]),
|
|
189
|
+
AgentMessageFactory.createToolResultsMessage([toolResultEntry], ctx.config.passToolBinariesToModel !== false),
|
|
214
190
|
];
|
|
215
191
|
|
|
216
192
|
const loopResult = await this.runTurnLoopUntilFinalAnswer({
|
|
@@ -236,16 +212,10 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
236
212
|
return this.buildOutputItem(item, outputJson).json;
|
|
237
213
|
}
|
|
238
214
|
|
|
239
|
-
/**
|
|
240
|
-
* Normalizes a {@link ResumeContext} decision into a flat JSON-serializable shape
|
|
241
|
-
* suitable for injection as a tool_result content.
|
|
242
|
-
*/
|
|
243
215
|
private normalizeDecision(resumeContext: ResumeContext): Record<string, unknown> {
|
|
244
216
|
const { decision } = resumeContext;
|
|
245
217
|
if (decision.kind === "decided") {
|
|
246
218
|
const value = decision.value as Record<string, unknown> | null | undefined;
|
|
247
|
-
// Convention: the decision schema for an approval tool has { approved: boolean, note?: string }.
|
|
248
|
-
// The status is "approved" when approved === true, otherwise "rejected".
|
|
249
219
|
const isApproved =
|
|
250
220
|
typeof value === "object" && value !== null && "approved" in value ? Boolean(value["approved"]) : true;
|
|
251
221
|
return {
|
|
@@ -258,7 +228,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
258
228
|
if (decision.kind === "timed_out") {
|
|
259
229
|
return { status: "timed_out", at: decision.at.toISOString() };
|
|
260
230
|
}
|
|
261
|
-
// auto_accepted
|
|
262
231
|
return { status: "auto_accepted", at: decision.at.toISOString() };
|
|
263
232
|
}
|
|
264
233
|
|
|
@@ -287,7 +256,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
287
256
|
);
|
|
288
257
|
const resolvedTools = this.resolveTools(ctx.config.tools ?? []);
|
|
289
258
|
|
|
290
|
-
// Resolve MCP tools when the config declares mcpServers and the integration is registered.
|
|
291
259
|
const mcpToolsByServer = await this.prepareMcpToolsByServer(ctx);
|
|
292
260
|
|
|
293
261
|
const toolLoadingStrategy = await this.toolLoadingStrategyFactory.create({
|
|
@@ -331,7 +299,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
331
299
|
itemIndex: ctx.itemIndex,
|
|
332
300
|
parentInvocationId: ctx.parentInvocationId,
|
|
333
301
|
});
|
|
334
|
-
// Cast from AgentMcpToolMap (core contract, no ai dependency) to ToolSet (ai SDK type).
|
|
335
302
|
return toolMap as unknown as ReadonlyMap<string, ToolSet>;
|
|
336
303
|
}
|
|
337
304
|
|
|
@@ -383,24 +350,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
383
350
|
return this.buildOutputItem(item, outputJson);
|
|
384
351
|
}
|
|
385
352
|
|
|
386
|
-
/**
|
|
387
|
-
* Multi-turn loop:
|
|
388
|
-
* - Each turn is a single `generateText` call with tools exposed but **not auto-executed**
|
|
389
|
-
* (we control tool dispatch so that {@link AgentToolExecutionCoordinator} drives repair /
|
|
390
|
-
* connection-invocation recording / transient-error handling exactly like before).
|
|
391
|
-
* - When the model returns no tool calls the loop ends with the model's text as the final answer.
|
|
392
|
-
* - Respects `guardrails.maxTurns` and `guardrails.onTurnLimitReached`.
|
|
393
|
-
* - Strategy-owned tool calls (e.g. `find_tools`) are dispatched via the strategy, not the
|
|
394
|
-
* coordinator; their results are tracked so subsequent turns receive the discovered tools.
|
|
395
|
-
*/
|
|
396
353
|
private async runTurnLoopUntilFinalAnswer(args: {
|
|
397
354
|
prepared: PreparedAgentExecution;
|
|
398
355
|
itemInputsByPort: NodeInputsByPort;
|
|
399
356
|
itemScopedTools: ReadonlyArray<ItemScopedToolBinding>;
|
|
400
357
|
conversation: ModelMessage[];
|
|
401
|
-
/** When resuming from HITL suspension, the turn count at the point of suspension. */
|
|
402
358
|
resumedTurnCount?: number;
|
|
403
|
-
/** When resuming from HITL suspension, the tool-call count at the point of suspension. */
|
|
404
359
|
resumedToolCallCount?: number;
|
|
405
360
|
}): Promise<Readonly<{ finalText: string; turnCount: number; toolCallCount: number }>> {
|
|
406
361
|
const { prepared, itemInputsByPort, itemScopedTools, conversation } = args;
|
|
@@ -410,7 +365,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
410
365
|
let toolCallCount = args.resumedToolCallCount ?? 0;
|
|
411
366
|
let turnCount = 0;
|
|
412
367
|
const repairAttemptsByToolName = new Map<string, number>();
|
|
413
|
-
/** Tool IDs surfaced by find_tools across all prior turns in this item run. */
|
|
414
368
|
let previousFoundToolIds: ReadonlyArray<string> = [];
|
|
415
369
|
|
|
416
370
|
for (let turn = 1; turn <= guardrails.maxTurns; turn++) {
|
|
@@ -437,11 +391,9 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
437
391
|
break;
|
|
438
392
|
}
|
|
439
393
|
|
|
440
|
-
// Partition tool calls: strategy-owned (find_tools) vs coordinator-managed (node-backed).
|
|
441
394
|
const strategyOwnedCalls = result.toolCalls.filter((tc) => toolLoadingStrategy.ownsToolName(tc.name));
|
|
442
395
|
const coordinatorCalls = result.toolCalls.filter((tc) => !toolLoadingStrategy.ownsToolName(tc.name));
|
|
443
396
|
|
|
444
|
-
// Execute strategy-owned calls (find_tools) and track results for the next turn.
|
|
445
397
|
const strategyExecutedCalls: ExecutedToolCall[] = [];
|
|
446
398
|
for (const tc of strategyOwnedCalls) {
|
|
447
399
|
const metaResult = await toolLoadingStrategy.executeMetaTool(tc.name, tc.input);
|
|
@@ -459,14 +411,11 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
459
411
|
});
|
|
460
412
|
}
|
|
461
413
|
|
|
462
|
-
// Execute coordinator-managed calls if any.
|
|
463
414
|
const coordinatorExecutedCalls: ExecutedToolCall[] = [];
|
|
464
415
|
if (coordinatorCalls.length > 0) {
|
|
465
416
|
const plannedToolCalls = this.planToolCalls(itemScopedTools, coordinatorCalls, ctx.nodeId);
|
|
466
417
|
toolCallCount += plannedToolCalls.length;
|
|
467
418
|
await this.markQueuedTools(plannedToolCalls, ctx);
|
|
468
|
-
// Snapshot conversation with the assistant message appended — this is the checkpoint
|
|
469
|
-
// conversation the agent coordinator stores if a HITL tool suspends the run.
|
|
470
419
|
const assistantMsg =
|
|
471
420
|
result.assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(result.text, result.toolCalls);
|
|
472
421
|
const conversationWithAssistant: ModelMessage[] = [...conversation, assistantMsg];
|
|
@@ -490,6 +439,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
490
439
|
result.text,
|
|
491
440
|
result.toolCalls,
|
|
492
441
|
allExecutedCalls,
|
|
442
|
+
ctx.config.passToolBinariesToModel !== false,
|
|
493
443
|
);
|
|
494
444
|
}
|
|
495
445
|
|
|
@@ -522,10 +472,11 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
522
472
|
text: string,
|
|
523
473
|
toolCalls: ReadonlyArray<AgentToolCall>,
|
|
524
474
|
executedToolCalls: ReadonlyArray<ExecutedToolCall>,
|
|
475
|
+
passToolBinariesToModel: boolean,
|
|
525
476
|
): void {
|
|
526
477
|
conversation.push(
|
|
527
478
|
assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(text, toolCalls),
|
|
528
|
-
AgentMessageFactory.createToolResultsMessage(executedToolCalls),
|
|
479
|
+
AgentMessageFactory.createToolResultsMessage(executedToolCalls, passToolBinariesToModel),
|
|
529
480
|
);
|
|
530
481
|
}
|
|
531
482
|
|
|
@@ -609,12 +560,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
609
560
|
});
|
|
610
561
|
}
|
|
611
562
|
|
|
612
|
-
/**
|
|
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.
|
|
617
|
-
*/
|
|
618
563
|
private resolveHumanApprovalBehavior(config: ToolConfig): Readonly<{ onRejected: "halt" | "return" }> | undefined {
|
|
619
564
|
if (!this.isNodeBackedToolConfig(config)) return undefined;
|
|
620
565
|
const nodeConfig = config.node as unknown as { humanApprovalToolBehavior?: { onRejected?: "halt" | "return" } };
|
|
@@ -624,11 +569,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
624
569
|
return { onRejected: perBinding ?? marker?.onRejected ?? "return" };
|
|
625
570
|
}
|
|
626
571
|
|
|
627
|
-
/**
|
|
628
|
-
* Invoke a text turn using the merged tool set from item-scoped tools (coordinator-managed)
|
|
629
|
-
* and strategy tools (find_tools + discovered MCP tools).
|
|
630
|
-
* Strategy tools take precedence for names that overlap.
|
|
631
|
-
*/
|
|
632
572
|
private async invokeTextTurnWithStrategyTools(
|
|
633
573
|
prepared: PreparedAgentExecution,
|
|
634
574
|
itemInputsByPort: NodeInputsByPort,
|
|
@@ -638,18 +578,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
638
578
|
): Promise<TurnResult> {
|
|
639
579
|
const itemToolSet = this.buildToolSet(itemScopedTools);
|
|
640
580
|
const strategyHasTools = Object.keys(strategyTools).length > 0;
|
|
641
|
-
// Strip execute callbacks from strategy tools so the AI SDK does not auto-execute them.
|
|
642
|
-
// Codemation owns all tool dispatch (coordinator for node-backed, strategy for MCP/meta-tools).
|
|
643
581
|
const strippedStrategyTools = strategyHasTools ? this.stripExecuteCallbacks(strategyTools) : strategyTools;
|
|
644
582
|
const mergedTools: ToolSet | undefined =
|
|
645
583
|
itemToolSet || strategyHasTools ? { ...(itemToolSet ?? {}), ...strippedStrategyTools } : undefined;
|
|
646
584
|
return this.invokeTextTurnWithToolSet(prepared, itemInputsByPort, messages, mergedTools);
|
|
647
585
|
}
|
|
648
586
|
|
|
649
|
-
/**
|
|
650
|
-
* Removes `execute` properties from ToolSet entries so the AI SDK does not
|
|
651
|
-
* auto-execute them within `generateText`. Codemation owns all tool dispatch.
|
|
652
|
-
*/
|
|
653
587
|
private stripExecuteCallbacks(tools: ToolSet): ToolSet {
|
|
654
588
|
const stripped: Record<string, unknown> = {};
|
|
655
589
|
for (const [name, def] of Object.entries(tools)) {
|
|
@@ -659,12 +593,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
659
593
|
return stripped as ToolSet;
|
|
660
594
|
}
|
|
661
595
|
|
|
662
|
-
/**
|
|
663
|
-
* Builds a ToolSet from resolved tools for strategy initialization.
|
|
664
|
-
* The strategy uses this for its "always-included" node-backed tool descriptions.
|
|
665
|
-
* HITL tools (detected via the `humanApprovalToolBehavior` field set by `defineHumanApprovalNode`) get the solo-constraint sentence
|
|
666
|
-
* appended to their description.
|
|
667
|
-
*/
|
|
668
596
|
private buildToolSetFromResolved(resolvedTools: ReadonlyArray<ResolvedTool>): ToolSet {
|
|
669
597
|
if (resolvedTools.length === 0) return {};
|
|
670
598
|
const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
|
|
@@ -685,20 +613,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
685
613
|
return toolSet as unknown as ToolSet;
|
|
686
614
|
}
|
|
687
615
|
|
|
688
|
-
/**
|
|
689
|
-
* Builds an AI SDK {@link ToolSet} where every tool ships a pre-converted JSON Schema (via
|
|
690
|
-
* {@link jsonSchema}) — not the raw Zod schema — and carries **no** `execute`. Two reasons:
|
|
691
|
-
*
|
|
692
|
-
* 1. Codemation owns tool dispatch + the per-tool repair loop (see {@link AgentToolExecutionCoordinator}),
|
|
693
|
-
* so the AI SDK must surface tool calls back to us instead of auto-running them.
|
|
694
|
-
* 2. The AI SDK's `asSchema` helper discriminates between Zod v3 / Zod v4 / Standard Schema via
|
|
695
|
-
* runtime feature-detection (`~standard`, `_zod`, etc.). Handing it a pre-built
|
|
696
|
-
* {@link jsonSchema} record — which is tagged with `Symbol.for('vercel.ai.schema')` — skips all
|
|
697
|
-
* of that detection and guarantees the provider receives a draft-07 JSON Schema with
|
|
698
|
-
* `additionalProperties: false` at every object depth (see {@link OpenAiStrictJsonSchemaFactory}
|
|
699
|
-
* for the same logic applied to structured-output schemas). Codemation still runs its own Zod
|
|
700
|
-
* validation on tool inputs before execute — the schema handed to the model is advisory.
|
|
701
|
-
*/
|
|
702
616
|
private buildToolSet(itemScopedTools: ReadonlyArray<ItemScopedToolBinding>): ToolSet | undefined {
|
|
703
617
|
if (itemScopedTools.length === 0) return undefined;
|
|
704
618
|
const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
|
|
@@ -723,10 +637,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
723
637
|
return toolSet as unknown as ToolSet;
|
|
724
638
|
}
|
|
725
639
|
|
|
726
|
-
/**
|
|
727
|
-
* One `generateText` turn (no auto tool execution) with Codemation-owned child-span telemetry
|
|
728
|
-
* and connection-invocation state recording. Accepts a pre-built ToolSet.
|
|
729
|
-
*/
|
|
730
640
|
private async invokeTextTurnWithToolSet(
|
|
731
641
|
prepared: PreparedAgentExecution,
|
|
732
642
|
itemInputsByPort: NodeInputsByPort,
|
|
@@ -832,11 +742,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
832
742
|
}
|
|
833
743
|
}
|
|
834
744
|
|
|
835
|
-
/**
|
|
836
|
-
* Structured-output turn: runs `generateText({ output: Output.object({ schema }) })` via the
|
|
837
|
-
* structured-output runner. We keep this as a separate helper because the runner needs the raw
|
|
838
|
-
* validated value (not just text) back, and must be able to retry on Zod failures.
|
|
839
|
-
*/
|
|
840
745
|
private async invokeStructuredTurn(
|
|
841
746
|
prepared: PreparedAgentExecution,
|
|
842
747
|
itemInputsByPort: NodeInputsByPort,
|
|
@@ -886,11 +791,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
886
791
|
});
|
|
887
792
|
try {
|
|
888
793
|
const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
|
|
889
|
-
// Always feed the AI SDK a plain JSON Schema, never a raw Zod schema. A consumer
|
|
890
|
-
// workflow's outputSchema is created with the consumer's tsx-loaded Zod — a different
|
|
891
|
-
// runtime copy than the framework's Zod — so handing that object to `Output.object`
|
|
892
|
-
// throws "schema is not a function". Convert via the schema's own instance method
|
|
893
|
-
// (dual-zod safe; see AIAgentExecutionHelpersFactory) before wrapping with jsonSchema().
|
|
894
794
|
const schemaRecord = this.isZodSchema(schema)
|
|
895
795
|
? this.executionHelpers.createJsonSchemaRecord(schema, {
|
|
896
796
|
schemaName: structuredOptions?.schemaName ?? "structured_output",
|
|
@@ -977,13 +877,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
977
877
|
};
|
|
978
878
|
}
|
|
979
879
|
|
|
980
|
-
/**
|
|
981
|
-
* Build a no-code-friendly output payload for an LLM round.
|
|
982
|
-
*
|
|
983
|
-
* Always includes `content` (matching the canvas snapshot shape used elsewhere) and adds a
|
|
984
|
-
* `toolCalls` array when the round produced tool calls so the execution inspector surfaces the
|
|
985
|
-
* planned calls instead of just an empty `""` for tool-only rounds.
|
|
986
|
-
*/
|
|
987
880
|
private summarizeTurnOutput(turnResult: TurnResult): JsonValue {
|
|
988
881
|
if (turnResult.toolCalls.length === 0) return { content: turnResult.text };
|
|
989
882
|
const toolCalls = turnResult.toolCalls.map((toolCall) => ({
|
|
@@ -1237,19 +1130,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
1237
1130
|
});
|
|
1238
1131
|
const wrapped = this.wrapUntrustedSourceMessages(messages, item, ctx.config);
|
|
1239
1132
|
const promptMessages = AgentMessageFactory.createPromptMessages(wrapped);
|
|
1240
|
-
// Skip the passdown step entirely when the author opted out (default is on).
|
|
1241
1133
|
if (ctx.config.passBinariesToModel === false) return promptMessages;
|
|
1242
1134
|
const attachments = this.selectBinaryAttachments(item, itemIndex, items, ctx);
|
|
1243
1135
|
const binaries = await this.resolveInlineBinaries(attachments, ctx);
|
|
1244
1136
|
return AgentBinaryContentFactory.withBinaries(promptMessages, binaries);
|
|
1245
1137
|
}
|
|
1246
1138
|
|
|
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
1139
|
private selectBinaryAttachments(
|
|
1254
1140
|
item: Item,
|
|
1255
1141
|
itemIndex: number,
|
|
@@ -1263,14 +1149,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
1263
1149
|
return item.binary ? Object.values(item.binary) : [];
|
|
1264
1150
|
}
|
|
1265
1151
|
|
|
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
1152
|
private async resolveInlineBinaries(
|
|
1275
1153
|
attachments: ReadonlyArray<BinaryAttachment>,
|
|
1276
1154
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
@@ -1287,12 +1165,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
1287
1165
|
return resolved;
|
|
1288
1166
|
}
|
|
1289
1167
|
|
|
1290
|
-
/**
|
|
1291
|
-
* When `item.json.__source` matches an entry in `config.untrustedSources`
|
|
1292
|
-
* (default: `["gmail", "ocr", "webhook"]`), wraps every user-role message
|
|
1293
|
-
* content with an untrusted-external-source preamble so the LLM treats the
|
|
1294
|
-
* content as data, not instructions.
|
|
1295
|
-
*/
|
|
1296
1168
|
private wrapUntrustedSourceMessages(
|
|
1297
1169
|
messages: ReadonlyArray<AgentMessageDto>,
|
|
1298
1170
|
item: Item,
|
|
@@ -1388,5 +1260,3 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
1388
1260
|
return candidate.details;
|
|
1389
1261
|
}
|
|
1390
1262
|
}
|
|
1391
|
-
|
|
1392
|
-
// MARKER_12345
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import type { FilePart, ImagePart, ModelMessage, TextPart, UserModelMessage } from "ai";
|
|
2
2
|
|
|
3
|
-
/** A binary attachment already resolved to inline bytes, ready to become an AI SDK content part. */
|
|
4
3
|
export type ResolvedAgentBinary = Readonly<{
|
|
5
4
|
mediaType: string;
|
|
6
|
-
/** Base64-encoded bytes of the attachment. */
|
|
7
5
|
base64: string;
|
|
8
6
|
filename?: string;
|
|
9
7
|
}>;
|
|
10
8
|
|
|
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
9
|
export class AgentBinaryContentFactory {
|
|
22
10
|
static toContentPart(binary: ResolvedAgentBinary): ImagePart | FilePart {
|
|
23
11
|
if (binary.mediaType.startsWith("image/")) {
|
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import type { ModelMessage } from "ai";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Snapshot of the agent loop state at the moment of HITL suspension.
|
|
5
|
-
* Serialized as JSON and stored on `SuspensionRequest.request.metadata.agentCheckpoint`
|
|
6
|
-
* so the resumed node can reconstruct and continue the conversation.
|
|
7
|
-
*
|
|
8
|
-
* Defined here (story 10) and consumed in `AIAgentNode` resume branch.
|
|
9
|
-
*/
|
|
10
3
|
export type AgentLoopCheckpoint = Readonly<{
|
|
11
|
-
/** Full conversation history up to and including the assistant message that emitted tool_use. */
|
|
12
4
|
conversation: ModelMessage[];
|
|
13
|
-
/** Turn count at the point of suspension (1-based, matches loop counter in runTurnLoopUntilFinalAnswer). */
|
|
14
5
|
turnCount: number;
|
|
15
|
-
/** Total tool-call count accumulated before suspension. */
|
|
16
6
|
toolCallCount: number;
|
|
17
|
-
/** The tool_use id that triggered suspension; matched against the tool_result on resume. */
|
|
18
7
|
pendingToolCallId: string;
|
|
19
|
-
/** Display name of the agent (for logging / telemetry continuity). */
|
|
20
8
|
agentName: string;
|
|
21
|
-
/** Model identifier carried for migration-safety redundancy. */
|
|
22
9
|
modelId: string;
|
|
23
10
|
}>;
|
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import type { AgentMessageDto, AgentToolCall } from "@codemation/core";
|
|
2
2
|
|
|
3
|
-
import type { AssistantModelMessage, ModelMessage, ToolModelMessage } from "ai";
|
|
3
|
+
import type { AssistantModelMessage, ModelMessage, ToolModelMessage, ToolResultPart } from "ai";
|
|
4
4
|
|
|
5
|
+
import { AgentToolResultContentFactory } from "./AgentToolResultContentFactory";
|
|
5
6
|
import type { ExecutedToolCall } from "./aiAgentSupport.types";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
* AI-SDK-shaped message construction for the AIAgent stack. Emits plain `ModelMessage[]`
|
|
9
|
-
* ( `{ role: 'system' | 'user' | 'assistant' | 'tool', content: ... }` ) as consumed by
|
|
10
|
-
* `generateText({ messages })` from the `ai` package.
|
|
11
|
-
*/
|
|
12
8
|
export class AgentMessageFactory {
|
|
13
9
|
static createPromptMessages(messages: ReadonlyArray<AgentMessageDto>): ReadonlyArray<ModelMessage> {
|
|
14
10
|
return messages.map((message) => this.createPromptMessage(message));
|
|
15
11
|
}
|
|
16
12
|
|
|
17
|
-
/**
|
|
18
|
-
* Builds the assistant message that contains optional text plus one or more tool-call parts,
|
|
19
|
-
* matching the shape AI SDK emits between steps.
|
|
20
|
-
*/
|
|
21
13
|
static createAssistantWithToolCalls(
|
|
22
14
|
text: string | undefined,
|
|
23
15
|
toolCalls: ReadonlyArray<AgentToolCall>,
|
|
@@ -37,25 +29,31 @@ export class AgentMessageFactory {
|
|
|
37
29
|
return { role: "assistant", content };
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
static createToolResultsMessage(executedToolCalls: ReadonlyArray<ExecutedToolCall>): ToolModelMessage {
|
|
32
|
+
static createToolResultsMessage(
|
|
33
|
+
executedToolCalls: ReadonlyArray<ExecutedToolCall>,
|
|
34
|
+
passToolBinariesToModel = true,
|
|
35
|
+
): ToolModelMessage {
|
|
45
36
|
return {
|
|
46
37
|
role: "tool",
|
|
47
38
|
content: executedToolCalls.map((executed) => ({
|
|
48
39
|
type: "tool-result",
|
|
49
40
|
toolCallId: executed.toolCallId,
|
|
50
41
|
toolName: executed.toolName,
|
|
51
|
-
output:
|
|
52
|
-
type: "json",
|
|
53
|
-
value: AgentMessageFactory.toToolResultJson(executed.result),
|
|
54
|
-
},
|
|
42
|
+
output: AgentMessageFactory.toToolResultOutput(executed.result, passToolBinariesToModel),
|
|
55
43
|
})),
|
|
56
44
|
};
|
|
57
45
|
}
|
|
58
46
|
|
|
47
|
+
private static toToolResultOutput(result: unknown, passToolBinariesToModel: boolean): ToolResultPart["output"] {
|
|
48
|
+
if (passToolBinariesToModel) {
|
|
49
|
+
const content = AgentToolResultContentFactory.tryMapToContentOutput(result);
|
|
50
|
+
if (content !== undefined) {
|
|
51
|
+
return { type: "content", value: content };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { type: "json", value: AgentMessageFactory.toToolResultJson(result) };
|
|
55
|
+
}
|
|
56
|
+
|
|
59
57
|
private static toToolResultJson(value: unknown): import("ai").JSONValue {
|
|
60
58
|
if (value === undefined) return null;
|
|
61
59
|
try {
|
|
@@ -24,19 +24,6 @@ type ParsedStructuredOutputResult<TValue> = ParsedStructuredOutputSuccess<TValue
|
|
|
24
24
|
|
|
25
25
|
export type StructuredOutputSchemaForModel = ZodSchemaAny | Readonly<Record<string, unknown>>;
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Orchestrates a 2-attempt repair loop on top of `generateText({ output: Output.object(...) })`.
|
|
29
|
-
*
|
|
30
|
-
* Strategy:
|
|
31
|
-
* 1. If the caller already has a raw final text (from a prior tool-calling turn), try parsing it
|
|
32
|
-
* directly against the schema — fast path for models that already emit strict JSON.
|
|
33
|
-
* 2. Otherwise, run a native structured-output call via {@link invokeStructuredModel}. For the
|
|
34
|
-
* OpenAI-strict path, a {@link OpenAiStrictJsonSchemaFactory}-built JSON Schema record is
|
|
35
|
-
* handed to AI SDK's `jsonSchema(...)` wrapper (preserves `additionalProperties: false` at
|
|
36
|
-
* every object depth).
|
|
37
|
-
* 3. If the structured call fails (AI_NoObjectGeneratedError / ZodError / schema reject), run a
|
|
38
|
-
* text-mode repair prompt with the validation error appended, up to 2 attempts.
|
|
39
|
-
*/
|
|
40
27
|
@injectable()
|
|
41
28
|
export class AgentStructuredOutputRunner {
|
|
42
29
|
private static readonly repairAttemptCount = 2;
|
|
@@ -141,10 +128,6 @@ export class AgentStructuredOutputRunner {
|
|
|
141
128
|
);
|
|
142
129
|
}
|
|
143
130
|
|
|
144
|
-
/**
|
|
145
|
-
* Chooses strict mode for OpenAI chat-model configs, off otherwise. Extendable in future for
|
|
146
|
-
* other providers that adopt the same "supply a JSON Schema record directly" contract.
|
|
147
|
-
*/
|
|
148
131
|
private resolveStructuredOutputOptions(chatModelConfig: ChatModelConfig): StructuredOutputOptions | undefined {
|
|
149
132
|
if (chatModelConfig.type !== OpenAIChatModelFactory) {
|
|
150
133
|
return undefined;
|
|
@@ -27,18 +27,12 @@ export class AgentToolExecutionCoordinator {
|
|
|
27
27
|
ctx: NodeExecutionContext<AIAgent<any, any>>;
|
|
28
28
|
agentName: string;
|
|
29
29
|
repairAttemptsByToolName: Map<string, number>;
|
|
30
|
-
/** Conversation including the assistant message that emitted these tool_use blocks. Stored in checkpoint on HITL suspension. */
|
|
31
30
|
conversationSnapshot?: ReadonlyArray<ModelMessage>;
|
|
32
|
-
/** Turn count at the moment of this coordinator invocation. */
|
|
33
31
|
turnCount?: number;
|
|
34
|
-
/** Cumulative tool-call count up to and including this batch. */
|
|
35
32
|
toolCallCount?: number;
|
|
36
|
-
/** Model id for checkpoint migration safety. */
|
|
37
33
|
modelId?: string;
|
|
38
34
|
}>,
|
|
39
35
|
): Promise<ReadonlyArray<ExecutedToolCall>> {
|
|
40
|
-
// Solo enforcement: if any HITL tool appears alongside other tools, return error results
|
|
41
|
-
// for all calls so the model self-corrects on the next turn.
|
|
42
36
|
const hitlCalls = args.plannedToolCalls.filter((c) => c.binding.humanApproval !== undefined);
|
|
43
37
|
if (hitlCalls.length > 0 && args.plannedToolCalls.length > 1) {
|
|
44
38
|
return args.plannedToolCalls.map((c) => ({
|
|
@@ -68,7 +62,6 @@ export class AgentToolExecutionCoordinator {
|
|
|
68
62
|
const rejected = results.find((result) => result.status === "rejected");
|
|
69
63
|
if (rejected?.status === "rejected") {
|
|
70
64
|
const reason = rejected.reason;
|
|
71
|
-
// Preserve SuspensionRequest (not an Error subclass) before falling back to Error wrapping.
|
|
72
65
|
if (reason instanceof SuspensionRequest) throw reason;
|
|
73
66
|
throw reason instanceof Error ? reason : new Error(String(reason));
|
|
74
67
|
}
|
|
@@ -173,7 +166,6 @@ export class AgentToolExecutionCoordinator {
|
|
|
173
166
|
result,
|
|
174
167
|
} satisfies ExecutedToolCall;
|
|
175
168
|
} catch (error) {
|
|
176
|
-
// D1: Suspension catch — intercept before error classifier, augment with agent checkpoint.
|
|
177
169
|
if (error instanceof SuspensionRequest) {
|
|
178
170
|
const pendingToolCallId = plannedToolCall.toolCall.id ?? plannedToolCall.binding.config.name;
|
|
179
171
|
const checkpoint: AgentLoopCheckpoint = {
|
|
@@ -427,10 +419,6 @@ export class AgentToolExecutionCoordinator {
|
|
|
427
419
|
return candidate.details;
|
|
428
420
|
}
|
|
429
421
|
|
|
430
|
-
/**
|
|
431
|
-
* Extracts the text content from the last assistant message in the conversation snapshot.
|
|
432
|
-
* Used to populate `agentReasoning` in the HITL suspension metadata.
|
|
433
|
-
*/
|
|
434
422
|
private extractLastAssistantText(conversation: ReadonlyArray<ModelMessage>): string {
|
|
435
423
|
for (let i = conversation.length - 1; i >= 0; i--) {
|
|
436
424
|
const msg = conversation[i];
|