@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.cjs +180 -413
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +99 -1303
  5. package/dist/index.d.ts +99 -1303
  6. package/dist/index.js +181 -415
  7. package/dist/index.js.map +1 -1
  8. package/dist/metadata.json +1 -1
  9. package/package.json +2 -2
  10. package/src/authoring/defineRestNode.types.ts +0 -84
  11. package/src/canvasIconName.ts +0 -7
  12. package/src/chatModels/CodemationChatModelConfig.ts +0 -10
  13. package/src/chatModels/CodemationChatModelFactory.ts +0 -7
  14. package/src/chatModels/ManagedHmacSignerFactory.types.ts +0 -35
  15. package/src/chatModels/OpenAIChatModelFactory.ts +0 -2
  16. package/src/chatModels/OpenAiChatModelPresetsFactory.ts +0 -5
  17. package/src/chatModels/OpenAiCredentialSession.ts +0 -1
  18. package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +0 -21
  19. package/src/credentials/ApiKeyCredentialType.ts +0 -3
  20. package/src/credentials/BasicAuthCredentialType.ts +0 -4
  21. package/src/credentials/BearerTokenCredentialType.ts +0 -4
  22. package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +0 -19
  23. package/src/credentials/OAuth2TokenExchangeFactory.ts +0 -7
  24. package/src/http/HttpBodyBuilder.ts +0 -16
  25. package/src/http/HttpRequestExecutor.ts +0 -35
  26. package/src/http/HttpUrlBuilder.ts +0 -4
  27. package/src/http/SSRFBlockedError.ts +0 -4
  28. package/src/http/SsrfGuard.ts +10 -50
  29. package/src/http/httpRequest.types.ts +0 -49
  30. package/src/index.ts +1 -0
  31. package/src/nodes/AIAgentConfig.ts +3 -39
  32. package/src/nodes/AIAgentExecutionHelpersFactory.ts +0 -37
  33. package/src/nodes/AIAgentNode.ts +4 -134
  34. package/src/nodes/AgentBinaryContentFactory.ts +0 -12
  35. package/src/nodes/AgentLoopCheckpoint.types.ts +0 -13
  36. package/src/nodes/AgentMessageFactory.ts +17 -19
  37. package/src/nodes/AgentStructuredOutputRunner.ts +0 -17
  38. package/src/nodes/AgentToolExecutionCoordinator.ts +0 -12
  39. package/src/nodes/AgentToolResultContentFactory.ts +126 -0
  40. package/src/nodes/AssertionNode.ts +0 -14
  41. package/src/nodes/BM25Index.ts +0 -14
  42. package/src/nodes/ConnectionCredentialExecutionContextFactory.ts +0 -5
  43. package/src/nodes/ConnectionCredentialNode.ts +0 -4
  44. package/src/nodes/CronTriggerFactory.ts +0 -9
  45. package/src/nodes/DeferredMetaToolStrategy.ts +0 -18
  46. package/src/nodes/DeferredMetaToolStrategyFactory.ts +0 -5
  47. package/src/nodes/HttpRequestNodeFactory.ts +0 -14
  48. package/src/nodes/InboxApprovalNode.types.ts +0 -16
  49. package/src/nodes/IsTestRunNode.ts +0 -8
  50. package/src/nodes/ManualTriggerFactory.ts +0 -3
  51. package/src/nodes/ManualTriggerNode.ts +0 -4
  52. package/src/nodes/MergeNode.ts +0 -1
  53. package/src/nodes/NodeBackedToolRuntime.ts +0 -14
  54. package/src/nodes/SubWorkflowNode.ts +0 -3
  55. package/src/nodes/SwitchNode.ts +0 -3
  56. package/src/nodes/TestTriggerNode.ts +0 -9
  57. package/src/nodes/aiAgentSupport.types.ts +0 -16
  58. package/src/nodes/assertion.ts +0 -10
  59. package/src/nodes/codemationDocumentScannerNode.ts +0 -18
  60. package/src/nodes/collections/collectionListNode.types.ts +0 -1
  61. package/src/nodes/httpRequest.ts +0 -68
  62. package/src/nodes/isTestRun.ts +0 -4
  63. package/src/nodes/mapData.ts +0 -1
  64. package/src/nodes/merge.ts +0 -4
  65. package/src/nodes/mergeExecutionUtils.types.ts +0 -3
  66. package/src/nodes/nodeOptions.types.ts +0 -8
  67. package/src/nodes/schedulePollingTrigger.ts +37 -0
  68. package/src/nodes/split.ts +0 -4
  69. package/src/nodes/testTrigger.ts +0 -21
  70. package/src/nodes/wait.ts +0 -1
  71. package/src/nodes/webhookTriggerNode.ts +0 -5
  72. package/src/register.types.ts +0 -10
  73. package/src/workflows/AIAgentConnectionWorkflowExpander.ts +0 -3
@@ -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
- * Builds the `{ role: "tool", content: [{ type: "tool-result", ... }, ...] }` message returned
42
- * to the model after each tool round.
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];