@codemation/core-nodes 0.13.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 +13 -0
  2. package/dist/index.cjs +34 -433
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +95 -1312
  5. package/dist/index.d.ts +95 -1312
  6. package/dist/index.js +35 -435
  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 +0 -44
  32. package/src/nodes/AIAgentExecutionHelpersFactory.ts +0 -37
  33. package/src/nodes/AIAgentNode.ts +0 -132
  34. package/src/nodes/AgentBinaryContentFactory.ts +0 -12
  35. package/src/nodes/AgentLoopCheckpoint.types.ts +0 -13
  36. package/src/nodes/AgentMessageFactory.ts +0 -18
  37. package/src/nodes/AgentStructuredOutputRunner.ts +0 -17
  38. package/src/nodes/AgentToolExecutionCoordinator.ts +0 -12
  39. package/src/nodes/AgentToolResultContentFactory.ts +0 -29
  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,
@@ -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];
@@ -611,12 +560,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
611
560
  });
612
561
  }
613
562
 
614
- /**
615
- * Resolves the HITL behavior for a tool binding, or `undefined` when it is not a HITL tool.
616
- * A binding is HITL if either the backing node carries a `defineHumanApprovalNode` marker or the
617
- * binding sets a per-binding `onRejected` via `asTool(..., { onRejected })`. The per-binding value
618
- * wins over the node marker, so two tools backed by the same node can reject differently.
619
- */
620
563
  private resolveHumanApprovalBehavior(config: ToolConfig): Readonly<{ onRejected: "halt" | "return" }> | undefined {
621
564
  if (!this.isNodeBackedToolConfig(config)) return undefined;
622
565
  const nodeConfig = config.node as unknown as { humanApprovalToolBehavior?: { onRejected?: "halt" | "return" } };
@@ -626,11 +569,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
626
569
  return { onRejected: perBinding ?? marker?.onRejected ?? "return" };
627
570
  }
628
571
 
629
- /**
630
- * Invoke a text turn using the merged tool set from item-scoped tools (coordinator-managed)
631
- * and strategy tools (find_tools + discovered MCP tools).
632
- * Strategy tools take precedence for names that overlap.
633
- */
634
572
  private async invokeTextTurnWithStrategyTools(
635
573
  prepared: PreparedAgentExecution,
636
574
  itemInputsByPort: NodeInputsByPort,
@@ -640,18 +578,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
640
578
  ): Promise<TurnResult> {
641
579
  const itemToolSet = this.buildToolSet(itemScopedTools);
642
580
  const strategyHasTools = Object.keys(strategyTools).length > 0;
643
- // Strip execute callbacks from strategy tools so the AI SDK does not auto-execute them.
644
- // Codemation owns all tool dispatch (coordinator for node-backed, strategy for MCP/meta-tools).
645
581
  const strippedStrategyTools = strategyHasTools ? this.stripExecuteCallbacks(strategyTools) : strategyTools;
646
582
  const mergedTools: ToolSet | undefined =
647
583
  itemToolSet || strategyHasTools ? { ...(itemToolSet ?? {}), ...strippedStrategyTools } : undefined;
648
584
  return this.invokeTextTurnWithToolSet(prepared, itemInputsByPort, messages, mergedTools);
649
585
  }
650
586
 
651
- /**
652
- * Removes `execute` properties from ToolSet entries so the AI SDK does not
653
- * auto-execute them within `generateText`. Codemation owns all tool dispatch.
654
- */
655
587
  private stripExecuteCallbacks(tools: ToolSet): ToolSet {
656
588
  const stripped: Record<string, unknown> = {};
657
589
  for (const [name, def] of Object.entries(tools)) {
@@ -661,12 +593,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
661
593
  return stripped as ToolSet;
662
594
  }
663
595
 
664
- /**
665
- * Builds a ToolSet from resolved tools for strategy initialization.
666
- * The strategy uses this for its "always-included" node-backed tool descriptions.
667
- * HITL tools (detected via the `humanApprovalToolBehavior` field set by `defineHumanApprovalNode`) get the solo-constraint sentence
668
- * appended to their description.
669
- */
670
596
  private buildToolSetFromResolved(resolvedTools: ReadonlyArray<ResolvedTool>): ToolSet {
671
597
  if (resolvedTools.length === 0) return {};
672
598
  const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
@@ -687,20 +613,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
687
613
  return toolSet as unknown as ToolSet;
688
614
  }
689
615
 
690
- /**
691
- * Builds an AI SDK {@link ToolSet} where every tool ships a pre-converted JSON Schema (via
692
- * {@link jsonSchema}) — not the raw Zod schema — and carries **no** `execute`. Two reasons:
693
- *
694
- * 1. Codemation owns tool dispatch + the per-tool repair loop (see {@link AgentToolExecutionCoordinator}),
695
- * so the AI SDK must surface tool calls back to us instead of auto-running them.
696
- * 2. The AI SDK's `asSchema` helper discriminates between Zod v3 / Zod v4 / Standard Schema via
697
- * runtime feature-detection (`~standard`, `_zod`, etc.). Handing it a pre-built
698
- * {@link jsonSchema} record — which is tagged with `Symbol.for('vercel.ai.schema')` — skips all
699
- * of that detection and guarantees the provider receives a draft-07 JSON Schema with
700
- * `additionalProperties: false` at every object depth (see {@link OpenAiStrictJsonSchemaFactory}
701
- * for the same logic applied to structured-output schemas). Codemation still runs its own Zod
702
- * validation on tool inputs before execute — the schema handed to the model is advisory.
703
- */
704
616
  private buildToolSet(itemScopedTools: ReadonlyArray<ItemScopedToolBinding>): ToolSet | undefined {
705
617
  if (itemScopedTools.length === 0) return undefined;
706
618
  const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof import("ai").jsonSchema> }> =
@@ -725,10 +637,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
725
637
  return toolSet as unknown as ToolSet;
726
638
  }
727
639
 
728
- /**
729
- * One `generateText` turn (no auto tool execution) with Codemation-owned child-span telemetry
730
- * and connection-invocation state recording. Accepts a pre-built ToolSet.
731
- */
732
640
  private async invokeTextTurnWithToolSet(
733
641
  prepared: PreparedAgentExecution,
734
642
  itemInputsByPort: NodeInputsByPort,
@@ -834,11 +742,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
834
742
  }
835
743
  }
836
744
 
837
- /**
838
- * Structured-output turn: runs `generateText({ output: Output.object({ schema }) })` via the
839
- * structured-output runner. We keep this as a separate helper because the runner needs the raw
840
- * validated value (not just text) back, and must be able to retry on Zod failures.
841
- */
842
745
  private async invokeStructuredTurn(
843
746
  prepared: PreparedAgentExecution,
844
747
  itemInputsByPort: NodeInputsByPort,
@@ -888,11 +791,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
888
791
  });
889
792
  try {
890
793
  const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
891
- // Always feed the AI SDK a plain JSON Schema, never a raw Zod schema. A consumer
892
- // workflow's outputSchema is created with the consumer's tsx-loaded Zod — a different
893
- // runtime copy than the framework's Zod — so handing that object to `Output.object`
894
- // throws "schema is not a function". Convert via the schema's own instance method
895
- // (dual-zod safe; see AIAgentExecutionHelpersFactory) before wrapping with jsonSchema().
896
794
  const schemaRecord = this.isZodSchema(schema)
897
795
  ? this.executionHelpers.createJsonSchemaRecord(schema, {
898
796
  schemaName: structuredOptions?.schemaName ?? "structured_output",
@@ -979,13 +877,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
979
877
  };
980
878
  }
981
879
 
982
- /**
983
- * Build a no-code-friendly output payload for an LLM round.
984
- *
985
- * Always includes `content` (matching the canvas snapshot shape used elsewhere) and adds a
986
- * `toolCalls` array when the round produced tool calls so the execution inspector surfaces the
987
- * planned calls instead of just an empty `""` for tool-only rounds.
988
- */
989
880
  private summarizeTurnOutput(turnResult: TurnResult): JsonValue {
990
881
  if (turnResult.toolCalls.length === 0) return { content: turnResult.text };
991
882
  const toolCalls = turnResult.toolCalls.map((toolCall) => ({
@@ -1239,19 +1130,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
1239
1130
  });
1240
1131
  const wrapped = this.wrapUntrustedSourceMessages(messages, item, ctx.config);
1241
1132
  const promptMessages = AgentMessageFactory.createPromptMessages(wrapped);
1242
- // Skip the passdown step entirely when the author opted out (default is on).
1243
1133
  if (ctx.config.passBinariesToModel === false) return promptMessages;
1244
1134
  const attachments = this.selectBinaryAttachments(item, itemIndex, items, ctx);
1245
1135
  const binaries = await this.resolveInlineBinaries(attachments, ctx);
1246
1136
  return AgentBinaryContentFactory.withBinaries(promptMessages, binaries);
1247
1137
  }
1248
1138
 
1249
- /**
1250
- * Picks which attachments feed the passdown. When the author supplies `config.binaries`
1251
- * (a static array or a per-item function — e.g. to forward binaries from an earlier node),
1252
- * those replace the current item's attachments; otherwise the current item's `item.binary`
1253
- * is used.
1254
- */
1255
1139
  private selectBinaryAttachments(
1256
1140
  item: Item,
1257
1141
  itemIndex: number,
@@ -1265,14 +1149,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
1265
1149
  return item.binary ? Object.values(item.binary) : [];
1266
1150
  }
1267
1151
 
1268
- /**
1269
- * Reads every attachment through `ctx.binary` (storage-backed, by reference — never base64 on
1270
- * `item.json`) and resolves it to inline base64 so the agent can pass it to the chat model as a
1271
- * native multimodal block. Images become image blocks; every other type (PDF, office docs, CSV,
1272
- * JSON, …) becomes a file block — we don't filter by media type, so any binary can be fed to the
1273
- * model. If the provider rejects an unsupported type the error surfaces at runtime, and the
1274
- * workflow can filter the binary upstream.
1275
- */
1276
1152
  private async resolveInlineBinaries(
1277
1153
  attachments: ReadonlyArray<BinaryAttachment>,
1278
1154
  ctx: NodeExecutionContext<AIAgent<any, any>>,
@@ -1289,12 +1165,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
1289
1165
  return resolved;
1290
1166
  }
1291
1167
 
1292
- /**
1293
- * When `item.json.__source` matches an entry in `config.untrustedSources`
1294
- * (default: `["gmail", "ocr", "webhook"]`), wraps every user-role message
1295
- * content with an untrusted-external-source preamble so the LLM treats the
1296
- * content as data, not instructions.
1297
- */
1298
1168
  private wrapUntrustedSourceMessages(
1299
1169
  messages: ReadonlyArray<AgentMessageDto>,
1300
1170
  item: Item,
@@ -1390,5 +1260,3 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
1390
1260
  return candidate.details;
1391
1261
  }
1392
1262
  }
1393
-
1394
- // 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
  }>;
@@ -5,20 +5,11 @@ import type { AssistantModelMessage, ModelMessage, ToolModelMessage, ToolResultP
5
5
  import { AgentToolResultContentFactory } from "./AgentToolResultContentFactory";
6
6
  import type { ExecutedToolCall } from "./aiAgentSupport.types";
7
7
 
8
- /**
9
- * AI-SDK-shaped message construction for the AIAgent stack. Emits plain `ModelMessage[]`
10
- * ( `{ role: 'system' | 'user' | 'assistant' | 'tool', content: ... }` ) as consumed by
11
- * `generateText({ messages })` from the `ai` package.
12
- */
13
8
  export class AgentMessageFactory {
14
9
  static createPromptMessages(messages: ReadonlyArray<AgentMessageDto>): ReadonlyArray<ModelMessage> {
15
10
  return messages.map((message) => this.createPromptMessage(message));
16
11
  }
17
12
 
18
- /**
19
- * Builds the assistant message that contains optional text plus one or more tool-call parts,
20
- * matching the shape AI SDK emits between steps.
21
- */
22
13
  static createAssistantWithToolCalls(
23
14
  text: string | undefined,
24
15
  toolCalls: ReadonlyArray<AgentToolCall>,
@@ -38,10 +29,6 @@ export class AgentMessageFactory {
38
29
  return { role: "assistant", content };
39
30
  }
40
31
 
41
- /**
42
- * Builds the `{ role: "tool", content: [{ type: "tool-result", ... }, ...] }` message returned
43
- * to the model after each tool round.
44
- */
45
32
  static createToolResultsMessage(
46
33
  executedToolCalls: ReadonlyArray<ExecutedToolCall>,
47
34
  passToolBinariesToModel = true,
@@ -57,11 +44,6 @@ export class AgentMessageFactory {
57
44
  };
58
45
  }
59
46
 
60
- /**
61
- * Routes a tool result to a native multimodal `{ type: "content" }` output when it is
62
- * content-block-shaped (an MCP `CallToolResult`) and binary passdown is enabled; otherwise keeps
63
- * the inert `{ type: "json" }` path.
64
- */
65
47
  private static toToolResultOutput(result: unknown, passToolBinariesToModel: boolean): ToolResultPart["output"] {
66
48
  if (passToolBinariesToModel) {
67
49
  const content = AgentToolResultContentFactory.tryMapToContentOutput(result);
@@ -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];
@@ -1,22 +1,12 @@
1
1
  import type { ToolResultPart } from "ai";
2
2
 
3
- /** The `value` array of a `{ type: "content" }` tool-result output, as accepted by the AI SDK. */
4
3
  type ContentOutputValue = Extract<ToolResultPart["output"], { type: "content" }>["value"];
5
4
  type ContentOutputPart = ContentOutputValue[number];
6
5
 
7
- /**
8
- * Cap on raw (pre-base64) bytes inlined from a single tool result. Base64 inflates ~33% and every
9
- * inlined byte eats model context, so oversize binaries are replaced with a text placeholder.
10
- */
11
6
  const MAX_INLINE_BYTES = 8 * 1024 * 1024;
12
7
 
13
- /** MCP content-block discriminators. At least one must appear for a result to be treated as MCP-shaped. */
14
8
  const KNOWN_MCP_BLOCK_TYPES: ReadonlySet<string> = new Set(["text", "image", "audio", "resource", "resource_link"]);
15
9
 
16
- /**
17
- * MCP-style content block, as returned verbatim by a `CallToolResult` (`@ai-sdk/mcp` tool `execute`).
18
- * Only the fields this factory reads are modelled; unknown shapes fall through to a text marker.
19
- */
20
10
  type McpContentBlock = Readonly<{
21
11
  type?: unknown;
22
12
  text?: unknown;
@@ -27,19 +17,6 @@ type McpContentBlock = Readonly<{
27
17
  resource?: Readonly<{ blob?: unknown; text?: unknown; mimeType?: unknown; uri?: unknown; name?: unknown }>;
28
18
  }>;
29
19
 
30
- /**
31
- * Maps a tool result that is **content-block-shaped** (an MCP `CallToolResult` with a `content`
32
- * array) into AI SDK `{ type: "content" }` tool-result output, so binaries reach the chat model as
33
- * native multimodal tool-result blocks instead of being flattened to inert JSON text.
34
- *
35
- * The `@ai-sdk/anthropic` provider maps a `content`-output part as follows:
36
- * `text` → text block, `image-data` → image block, `file-data` (only `application/pdf`) → document
37
- * block. Non-PDF `file-data` is dropped by the provider, so this factory emits `image-data` for
38
- * images, `file-data` only for PDFs, and a text marker for every other binary type.
39
- *
40
- * Returns `undefined` when the result is not content-block-shaped — callers keep the existing
41
- * `{ type: "json" }` path, so plain string/object tool results are unaffected.
42
- */
43
20
  export class AgentToolResultContentFactory {
44
21
  static tryMapToContentOutput(result: unknown): ContentOutputValue | undefined {
45
22
  const blocks = AgentToolResultContentFactory.contentBlocks(result);
@@ -55,12 +32,6 @@ export class AgentToolResultContentFactory {
55
32
  return parts;
56
33
  }
57
34
 
58
- /**
59
- * Returns the `content` array iff `result` is an object whose `content` is an array of typed
60
- * blocks AND at least one block carries a known MCP discriminator. A plain JSON result that merely
61
- * has a `content` key of some other shape (e.g. Notion/Slack rich-text blocks) is rejected,
62
- * preserving the `{ type: "json" }` path so its payload is never lost.
63
- */
64
35
  private static contentBlocks(result: unknown): ReadonlyArray<McpContentBlock> | undefined {
65
36
  if (result === null || typeof result !== "object") return undefined;
66
37
  const content = (result as { content?: unknown }).content;
@@ -3,17 +3,6 @@ import { node } from "@codemation/core";
3
3
 
4
4
  import { Assertion } from "./assertion";
5
5
 
6
- /**
7
- * Runs the author's `assertions` callback for each input item and emits one workflow `Item` per
8
- * returned {@link AssertionResult} on `main`. Persistence is handled by a host-side subscriber
9
- * to `nodeCompleted` events that filters on `config.emitsAssertions === true`; this node does
10
- * not write to any store on its own.
11
- *
12
- * If the author callback throws, we emit a single synthetic AssertionResult with `errored: true`
13
- * and `score: 0`. Without this catch the whole node would fail and no assertion row would be
14
- * persisted — making the rollup blind to "the assertion code itself is broken." The synthetic
15
- * row keeps `failedAssertionsByRunId` consistent and gives the UI something to surface.
16
- */
17
6
  @node({ packageName: "@codemation/core-nodes" })
18
7
  export class AssertionNode implements RunnableNode<Assertion<any>> {
19
8
  kind = "node" as const;
@@ -24,9 +13,6 @@ export class AssertionNode implements RunnableNode<Assertion<any>> {
24
13
  const config = ctx.config;
25
14
  try {
26
15
  const results: ReadonlyArray<AssertionResult> = await config.assertions(args.item, ctx);
27
- // Engine "array → fan-out on main, each element is item.json" — returning the plain results
28
- // makes downstream `item.json` exactly an AssertionResult. Wrapping in `{ json: result }`
29
- // would double-wrap (engine would see `Item`-shaped values but treat them as JSON values).
30
16
  return [...results];
31
17
  } catch (err) {
32
18
  const message = err instanceof Error ? err.message : String(err);
@@ -1,9 +1,3 @@
1
- /**
2
- * Minimal BM25 (Okapi BM25) implementation for indexing MCP tool descriptions.
3
- *
4
- * Parameters: k1=1.5, b=0.75 (standard defaults).
5
- * Tokenisation: lowercase, split on non-alphanumerics, filter empties.
6
- */
7
1
  export class BM25Index {
8
2
  private readonly k1 = 1.5;
9
3
  private readonly b = 0.75;
@@ -12,10 +6,6 @@ export class BM25Index {
12
6
  private readonly df = new Map<string, number>();
13
7
  private avgDocLen = 0;
14
8
 
15
- /**
16
- * Add all documents at once. After calling this, search is available.
17
- * Documents are indexed in insertion order; search returns their indices.
18
- */
19
9
  add(docs: ReadonlyArray<string>): void {
20
10
  const docTerms = docs.map((d) => this.tokenize(d));
21
11
 
@@ -34,10 +24,6 @@ export class BM25Index {
34
24
  this.avgDocLen = docTerms.length > 0 ? totalLen / docTerms.length : 0;
35
25
  }
36
26
 
37
- /**
38
- * Returns up to `limit` document indices ranked by BM25 score (highest first).
39
- * Returns an empty array if the index is empty or the query matches nothing.
40
- */
41
27
  search(query: string, limit: number): ReadonlyArray<number> {
42
28
  const n = this.tf.length;
43
29
  if (n === 0) return [];
@@ -8,11 +8,6 @@ import type {
8
8
 
9
9
  import { CredentialResolverFactory } from "@codemation/core/bootstrap";
10
10
 
11
- /**
12
- * Builds a {@link NodeExecutionContext} whose identity for credential binding and `getCredential`
13
- * is a **connection-owned** workflow node id (`ConnectionNodeIdFactory` in `@codemation/core`),
14
- * not the executing parent node. Use for LLM slots, tool slots, or any connection-scoped owner.
15
- */
16
11
  export class ConnectionCredentialExecutionContextFactory {
17
12
  private readonly credentialResolverFactory: CredentialResolverFactory;
18
13