@codemation/core-nodes 0.4.3 → 0.7.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 (55) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/dist/index.cjs +3541 -470
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1843 -685
  5. package/dist/index.d.ts +1843 -685
  6. package/dist/index.js +3498 -465
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -5
  9. package/src/authoring/defineRestNode.types.ts +204 -0
  10. package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
  11. package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
  12. package/src/credentials/ApiKeyCredentialType.ts +60 -0
  13. package/src/credentials/BasicAuthCredentialType.ts +51 -0
  14. package/src/credentials/BearerTokenCredentialType.ts +40 -0
  15. package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
  16. package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
  17. package/src/credentials/index.ts +4 -0
  18. package/src/http/HttpBodyBuilder.ts +118 -0
  19. package/src/http/HttpRequestExecutor.ts +153 -0
  20. package/src/http/HttpUrlBuilder.ts +22 -0
  21. package/src/http/httpRequest.types.ts +96 -0
  22. package/src/index.ts +10 -1
  23. package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
  24. package/src/nodes/AIAgentNode.ts +391 -288
  25. package/src/nodes/AgentMessageFactory.ts +57 -49
  26. package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
  27. package/src/nodes/AgentToolExecutionCoordinator.ts +31 -16
  28. package/src/nodes/AssertionNode.ts +42 -0
  29. package/src/nodes/CronTriggerFactory.ts +45 -0
  30. package/src/nodes/CronTriggerNode.ts +40 -0
  31. package/src/nodes/HttpRequestNodeFactory.ts +160 -16
  32. package/src/nodes/IsTestRunNode.ts +25 -0
  33. package/src/nodes/NodeBackedToolRuntime.ts +40 -4
  34. package/src/nodes/TestTriggerNode.ts +33 -0
  35. package/src/nodes/WebhookTriggerFactory.ts +1 -1
  36. package/src/nodes/aggregate.ts +1 -1
  37. package/src/nodes/aiAgentSupport.types.ts +22 -2
  38. package/src/nodes/assertion.ts +42 -0
  39. package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
  40. package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
  41. package/src/nodes/collections/collectionGetNode.types.ts +26 -0
  42. package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
  43. package/src/nodes/collections/collectionListNode.types.ts +30 -0
  44. package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
  45. package/src/nodes/collections/index.ts +6 -0
  46. package/src/nodes/httpRequest.ts +106 -1
  47. package/src/nodes/if.ts +1 -1
  48. package/src/nodes/isTestRun.ts +24 -0
  49. package/src/nodes/mapData.ts +1 -0
  50. package/src/nodes/merge.ts +1 -1
  51. package/src/nodes/noOp.ts +1 -0
  52. package/src/nodes/split.ts +1 -1
  53. package/src/nodes/testTrigger.ts +72 -0
  54. package/src/nodes/wait.ts +1 -0
  55. package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
@@ -1,19 +1,19 @@
1
1
  import type {
2
2
  AgentGuardrailConfig,
3
3
  AgentToolCall,
4
+ ChatLanguageModel,
5
+ ChatLanguageModelCallOptions,
4
6
  ChatModelConfig,
5
7
  ChatModelFactory,
6
8
  Item,
7
9
  Items,
8
10
  JsonValue,
9
- LangChainChatModelLike,
10
11
  NodeExecutionContext,
11
12
  NodeInputsByPort,
12
13
  RunnableNode,
13
14
  RunnableNodeExecuteArgs,
14
15
  Tool,
15
16
  ToolConfig,
16
- LangChainStructuredOutputModelLike,
17
17
  ZodSchemaAny,
18
18
  } from "@codemation/core";
19
19
 
@@ -34,7 +34,15 @@ import {
34
34
  type NodeResolver,
35
35
  } from "@codemation/core";
36
36
 
37
- import { AIMessage, type BaseMessage } from "@langchain/core/messages";
37
+ import type { AssistantModelMessage, GenerateTextResult, LanguageModel, ModelMessage, ToolSet } from "ai";
38
+ import { Output, generateText, jsonSchema } from "ai";
39
+
40
+ /**
41
+ * OUTPUT generic must extend AI SDK's `Output<OUTPUT, PARTIAL, ELEMENT>` which is parametric on
42
+ * `any`; there is no narrower concrete type we can substitute that accepts both text-only and
43
+ * structured turns uniformly.
44
+ */
45
+ type AnyGenerateTextResult = GenerateTextResult<ToolSet, any>;
38
46
  import { z } from "zod";
39
47
 
40
48
  import type { AIAgent } from "./AIAgentConfig";
@@ -60,25 +68,35 @@ type ResolvedGuardrails = Required<Pick<AgentGuardrailConfig, "maxTurns" | "onTu
60
68
  /** Everything needed to run the agent loop for one item (shared across items in the same activation). */
61
69
  interface PreparedAgentExecution {
62
70
  readonly ctx: NodeExecutionContext<AIAgent<any, any>>;
63
- readonly model: LangChainChatModelLike;
71
+ readonly model: ChatLanguageModel;
64
72
  readonly resolvedTools: ReadonlyArray<ResolvedTool>;
65
73
  readonly guardrails: ResolvedGuardrails;
66
74
  readonly languageModelConnectionNodeId: string;
67
75
  }
68
76
 
77
+ /** Result of one `generateText` turn with tools disabled for auto-execution. */
78
+ interface TurnResult {
79
+ readonly assistantMessage: AssistantModelMessage | undefined;
80
+ readonly text: string;
81
+ readonly toolCalls: ReadonlyArray<AgentToolCall>;
82
+ readonly usage: ModelUsage;
83
+ }
84
+
85
+ interface ModelUsage {
86
+ readonly inputTokens?: number;
87
+ readonly outputTokens?: number;
88
+ readonly totalTokens?: number;
89
+ readonly cachedInputTokens?: number;
90
+ readonly reasoningTokens?: number;
91
+ }
92
+
69
93
  @node({ packageName: "@codemation/core-nodes" })
70
94
  export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
71
95
  kind = "node" as const;
72
96
  outputPorts = ["main"] as const;
73
- /**
74
- * Engine validates {@link RunnableNodeConfig.inputSchema} (Zod) on {@code item.json} before enqueue, then resolves
75
- * per-item **`itemExpr`** leaves on config before {@link #execute}. Prefer modeling prompts as
76
- * {@code { messages: [{ role, content }, ...] }} (on input or config) so persisted inputs are visible in the UI.
77
- */
78
97
  readonly inputSchema = z.unknown();
79
98
 
80
99
  private readonly connectionCredentialExecutionContextFactory: ConnectionCredentialExecutionContextFactory;
81
- /** One resolved model/tools bundle per activation context (same ctx across items in a batch). */
82
100
  private readonly preparedByExecutionContext = new WeakMap<
83
101
  NodeExecutionContext<AIAgent<any, any>>,
84
102
  Promise<PreparedAgentExecution>
@@ -123,9 +141,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
123
141
  }
124
142
  }
125
143
 
126
- /**
127
- * Resolves the chat model and tools once per activation, then reuses for every item in the batch.
128
- */
129
144
  private async prepareExecution(ctx: NodeExecutionContext<AIAgent<any, any>>): Promise<PreparedAgentExecution> {
130
145
  const chatModelFactory = this.nodeResolver.resolve(ctx.config.chatModel.type) as ChatModelFactory<ChatModelConfig>;
131
146
  const languageModelCredentialContext = this.connectionCredentialExecutionContextFactory.forConnectionNode(ctx, {
@@ -144,9 +159,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
144
159
  };
145
160
  }
146
161
 
147
- /**
148
- * One item: build prompts, optionally bind tools, run the multi-turn loop, map the final model message to workflow JSON.
149
- */
150
162
  private async runAgentForItem(
151
163
  prepared: PreparedAgentExecution,
152
164
  item: Item,
@@ -156,7 +168,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
156
168
  const { ctx } = prepared;
157
169
  const itemInputsByPort = AgentItemPortMap.fromItem(item);
158
170
  const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, itemIndex, items);
159
- const conversation: BaseMessage[] = [...this.createPromptMessages(item, itemIndex, items, ctx)];
171
+ const conversation: ModelMessage[] = [...this.createPromptMessages(item, itemIndex, items, ctx)];
160
172
  if (ctx.config.outputSchema && itemScopedTools.length === 0) {
161
173
  const structuredOutput = await this.structuredOutputRunner.resolve({
162
174
  model: prepared.model,
@@ -165,36 +177,19 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
165
177
  conversation,
166
178
  agentName: this.getAgentDisplayName(ctx),
167
179
  nodeId: ctx.nodeId,
168
- invokeTextModel: async (messages) =>
169
- await this.invokeModel(
170
- prepared.model,
171
- prepared.languageModelConnectionNodeId,
172
- messages,
173
- ctx,
174
- itemInputsByPort,
175
- prepared.guardrails.modelInvocationOptions,
176
- ),
177
- invokeStructuredModel: async (structuredModel, messages) =>
178
- await this.invokeStructuredModel(
179
- structuredModel,
180
- prepared.languageModelConnectionNodeId,
181
- messages,
182
- ctx,
183
- itemInputsByPort,
184
- prepared.guardrails.modelInvocationOptions,
185
- ),
180
+ invokeTextModel: async (messages) => await this.invokeTextTurn(prepared, itemInputsByPort, messages, []),
181
+ invokeStructuredModel: async (schema, messages, structuredOptions) =>
182
+ await this.invokeStructuredTurn(prepared, itemInputsByPort, schema, messages, structuredOptions),
186
183
  });
187
184
  await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentTurns, value: 1 });
188
185
  await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentToolCalls, value: 0 });
189
186
  return this.buildOutputItem(item, structuredOutput);
190
187
  }
191
- const modelWithTools = this.bindToolsToModel(prepared.model, itemScopedTools);
192
188
  const loopResult = await this.runTurnLoopUntilFinalAnswer({
193
189
  prepared,
194
190
  itemInputsByPort,
195
191
  itemScopedTools,
196
192
  conversation,
197
- modelWithTools,
198
193
  });
199
194
  await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentTurns, value: loopResult.turnCount });
200
195
  await ctx.telemetry.recordMetric({
@@ -205,44 +200,40 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
205
200
  prepared,
206
201
  itemInputsByPort,
207
202
  conversation,
208
- loopResult.finalResponse,
203
+ loopResult.finalText,
209
204
  itemScopedTools.length > 0,
210
205
  );
211
206
  return this.buildOutputItem(item, outputJson);
212
207
  }
213
208
 
214
209
  /**
215
- * Repeatedly invokes the model until it returns without tool calls, or guardrails end the loop.
210
+ * Multi-turn loop:
211
+ * - Each turn is a single `generateText` call with tools exposed but **not auto-executed**
212
+ * (we control tool dispatch so that {@link AgentToolExecutionCoordinator} drives repair /
213
+ * connection-invocation recording / transient-error handling exactly like before).
214
+ * - When the model returns no tool calls the loop ends with the model's text as the final answer.
215
+ * - Respects `guardrails.maxTurns` and `guardrails.onTurnLimitReached`.
216
216
  */
217
217
  private async runTurnLoopUntilFinalAnswer(args: {
218
218
  prepared: PreparedAgentExecution;
219
219
  itemInputsByPort: NodeInputsByPort;
220
220
  itemScopedTools: ReadonlyArray<ItemScopedToolBinding>;
221
- conversation: BaseMessage[];
222
- modelWithTools: LangChainChatModelLike;
223
- }): Promise<Readonly<{ finalResponse: AIMessage; turnCount: number; toolCallCount: number }>> {
224
- const { prepared, itemInputsByPort, itemScopedTools, conversation, modelWithTools } = args;
225
- const { ctx, guardrails, languageModelConnectionNodeId } = prepared;
221
+ conversation: ModelMessage[];
222
+ }): Promise<Readonly<{ finalText: string; turnCount: number; toolCallCount: number }>> {
223
+ const { prepared, itemInputsByPort, itemScopedTools, conversation } = args;
224
+ const { ctx, guardrails } = prepared;
226
225
 
227
- let finalResponse: AIMessage | undefined;
226
+ let finalText = "";
228
227
  let toolCallCount = 0;
229
228
  let turnCount = 0;
230
229
  const repairAttemptsByToolName = new Map<string, number>();
231
230
 
232
231
  for (let turn = 1; turn <= guardrails.maxTurns; turn++) {
233
232
  turnCount = turn;
234
- const response = await this.invokeModel(
235
- modelWithTools,
236
- languageModelConnectionNodeId,
237
- conversation,
238
- ctx,
239
- itemInputsByPort,
240
- guardrails.modelInvocationOptions,
241
- );
242
- finalResponse = response;
233
+ const result = await this.invokeTextTurn(prepared, itemInputsByPort, conversation, itemScopedTools);
234
+ finalText = result.text;
243
235
 
244
- const toolCalls = AgentMessageFactory.extractToolCalls(response);
245
- if (toolCalls.length === 0) {
236
+ if (result.toolCalls.length === 0) {
246
237
  break;
247
238
  }
248
239
 
@@ -251,7 +242,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
251
242
  break;
252
243
  }
253
244
 
254
- const plannedToolCalls = this.planToolCalls(itemScopedTools, toolCalls, ctx.nodeId);
245
+ const plannedToolCalls = this.planToolCalls(itemScopedTools, result.toolCalls, ctx.nodeId);
255
246
  toolCallCount += plannedToolCalls.length;
256
247
  await this.markQueuedTools(plannedToolCalls, ctx);
257
248
  const executedToolCalls = await this.toolExecutionCoordinator.execute({
@@ -260,14 +251,17 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
260
251
  agentName: this.getAgentDisplayName(ctx),
261
252
  repairAttemptsByToolName,
262
253
  });
263
- this.appendAssistantAndToolMessages(conversation, response, executedToolCalls);
254
+ this.appendAssistantAndToolMessages(
255
+ conversation,
256
+ result.assistantMessage,
257
+ result.text,
258
+ result.toolCalls,
259
+ executedToolCalls,
260
+ );
264
261
  }
265
262
 
266
- if (!finalResponse) {
267
- throw new Error(`AIAgent "${ctx.config.name ?? ctx.nodeId}" did not produce a model response.`);
268
- }
269
263
  return {
270
- finalResponse,
264
+ finalText,
271
265
  turnCount,
272
266
  toolCallCount,
273
267
  };
@@ -290,54 +284,42 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
290
284
  }
291
285
 
292
286
  private appendAssistantAndToolMessages(
293
- conversation: BaseMessage[],
294
- assistantMessage: AIMessage,
287
+ conversation: ModelMessage[],
288
+ assistantMessage: AssistantModelMessage | undefined,
289
+ text: string,
290
+ toolCalls: ReadonlyArray<AgentToolCall>,
295
291
  executedToolCalls: ReadonlyArray<ExecutedToolCall>,
296
292
  ): void {
297
293
  conversation.push(
298
- assistantMessage,
299
- ...executedToolCalls.map((toolCall) =>
300
- AgentMessageFactory.createToolMessage(toolCall.toolCallId, toolCall.serialized),
301
- ),
294
+ assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(text, toolCalls),
295
+ AgentMessageFactory.createToolResultsMessage(executedToolCalls),
302
296
  );
303
297
  }
304
298
 
305
299
  private async resolveFinalOutputJson(
306
300
  prepared: PreparedAgentExecution,
307
301
  itemInputsByPort: NodeInputsByPort,
308
- conversation: ReadonlyArray<BaseMessage>,
309
- finalResponse: AIMessage,
302
+ conversation: ReadonlyArray<ModelMessage>,
303
+ finalText: string,
310
304
  wasToolEnabledRun: boolean,
311
305
  ): Promise<unknown> {
312
306
  if (!prepared.ctx.config.outputSchema) {
313
- return AgentOutputFactory.fromAgentContent(AgentMessageFactory.extractContent(finalResponse));
307
+ return AgentOutputFactory.fromAgentContent(finalText);
314
308
  }
309
+ const conversationWithFinal: ReadonlyArray<ModelMessage> = wasToolEnabledRun
310
+ ? [...conversation, { role: "assistant", content: finalText }]
311
+ : conversation;
315
312
  return await this.structuredOutputRunner.resolve({
316
313
  model: prepared.model,
317
314
  chatModelConfig: prepared.ctx.config.chatModel,
318
315
  schema: prepared.ctx.config.outputSchema,
319
- conversation: wasToolEnabledRun ? [...conversation, finalResponse] : conversation,
320
- rawFinalResponse: finalResponse,
316
+ conversation: conversationWithFinal,
317
+ rawFinalText: finalText,
321
318
  agentName: this.getAgentDisplayName(prepared.ctx),
322
319
  nodeId: prepared.ctx.nodeId,
323
- invokeTextModel: async (messages) =>
324
- await this.invokeModel(
325
- prepared.model,
326
- prepared.languageModelConnectionNodeId,
327
- messages,
328
- prepared.ctx,
329
- itemInputsByPort,
330
- prepared.guardrails.modelInvocationOptions,
331
- ),
332
- invokeStructuredModel: async (structuredModel, messages) =>
333
- await this.invokeStructuredModel(
334
- structuredModel,
335
- prepared.languageModelConnectionNodeId,
336
- messages,
337
- prepared.ctx,
338
- itemInputsByPort,
339
- prepared.guardrails.modelInvocationOptions,
340
- ),
320
+ invokeTextModel: async (messages) => await this.invokeTextTurn(prepared, itemInputsByPort, messages, []),
321
+ invokeStructuredModel: async (schema, messages, structuredOptions) =>
322
+ await this.invokeStructuredTurn(prepared, itemInputsByPort, schema, messages, structuredOptions),
341
323
  });
342
324
  }
343
325
 
@@ -345,16 +327,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
345
327
  return AgentOutputFactory.replaceJson(item, outputJson);
346
328
  }
347
329
 
348
- private bindToolsToModel(
349
- model: LangChainChatModelLike,
350
- itemScopedTools: ReadonlyArray<ItemScopedToolBinding>,
351
- ): LangChainChatModelLike {
352
- if (itemScopedTools.length === 0 || !model.bindTools) {
353
- return model;
354
- }
355
- return model.bindTools(itemScopedTools.map((entry) => entry.langChainTool));
356
- }
357
-
358
330
  private resolveTools(toolConfigs: ReadonlyArray<ToolConfig>): ReadonlyArray<ResolvedTool> {
359
331
  const resolvedTools = toolConfigs.map((config) => ({
360
332
  config,
@@ -381,69 +353,147 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
381
353
  connectionNodeId: ConnectionNodeIdFactory.toolConnectionNodeId(ctx.nodeId, entry.config.name),
382
354
  getCredentialRequirements: () => entry.config.getCredentialRequirements?.() ?? [],
383
355
  });
384
- const langChainTool = this.executionHelpers.createDynamicStructuredTool(
385
- entry,
386
- toolCredentialContext,
387
- item,
388
- itemIndex,
389
- items,
390
- );
391
-
392
- return { config: entry.config, langChainTool };
356
+ return {
357
+ config: entry.config,
358
+ inputSchema: entry.runtime.inputSchema,
359
+ execute: async (input, hooks): Promise<unknown> => {
360
+ const validated = entry.runtime.inputSchema.parse(input) as unknown;
361
+ return await entry.runtime.execute({
362
+ config: entry.config,
363
+ input: validated,
364
+ ctx: toolCredentialContext,
365
+ item,
366
+ itemIndex,
367
+ items,
368
+ hooks,
369
+ });
370
+ },
371
+ } satisfies ItemScopedToolBinding;
393
372
  });
394
373
  }
395
374
 
396
- private async invokeModel(
397
- model: LangChainChatModelLike,
398
- nodeId: string,
399
- messages: ReadonlyArray<BaseMessage>,
400
- ctx: NodeExecutionContext<AIAgent<any, any>>,
401
- inputsByPort: NodeInputsByPort,
402
- options?: AgentGuardrailConfig["modelInvocationOptions"],
403
- ): Promise<AIMessage> {
375
+ /**
376
+ * Builds an AI SDK {@link ToolSet} where every tool ships a pre-converted JSON Schema (via
377
+ * {@link jsonSchema}) — not the raw Zod schema — and carries **no** `execute`. Two reasons:
378
+ *
379
+ * 1. Codemation owns tool dispatch + the per-tool repair loop (see {@link AgentToolExecutionCoordinator}),
380
+ * so the AI SDK must surface tool calls back to us instead of auto-running them.
381
+ * 2. The AI SDK's `asSchema` helper discriminates between Zod v3 / Zod v4 / Standard Schema via
382
+ * runtime feature-detection (`~standard`, `_zod`, etc.). Handing it a pre-built
383
+ * {@link jsonSchema} record — which is tagged with `Symbol.for('vercel.ai.schema')` — skips all
384
+ * of that detection and guarantees the provider receives a draft-07 JSON Schema with
385
+ * `additionalProperties: false` at every object depth (see {@link OpenAiStrictJsonSchemaFactory}
386
+ * for the same logic applied to structured-output schemas). Codemation still runs its own Zod
387
+ * validation on tool inputs before execute — the schema handed to the model is advisory.
388
+ */
389
+ private buildToolSet(itemScopedTools: ReadonlyArray<ItemScopedToolBinding>): ToolSet | undefined {
390
+ if (itemScopedTools.length === 0) return undefined;
391
+ const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof jsonSchema> }> = {};
392
+ for (const entry of itemScopedTools) {
393
+ const schemaRecord = this.executionHelpers.createJsonSchemaRecord(entry.inputSchema, {
394
+ schemaName: entry.config.name,
395
+ requireObjectRoot: true,
396
+ });
397
+ toolSet[entry.config.name] = {
398
+ description: entry.config.description,
399
+ inputSchema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]),
400
+ };
401
+ }
402
+ return toolSet as unknown as ToolSet;
403
+ }
404
+
405
+ /**
406
+ * One `generateText` turn (no auto tool execution) with Codemation-owned child-span telemetry
407
+ * and connection-invocation state recording.
408
+ */
409
+ private async invokeTextTurn(
410
+ prepared: PreparedAgentExecution,
411
+ itemInputsByPort: NodeInputsByPort,
412
+ messages: ReadonlyArray<ModelMessage>,
413
+ itemScopedTools: ReadonlyArray<ItemScopedToolBinding>,
414
+ ): Promise<TurnResult> {
404
415
  const invocationId = ConnectionInvocationIdFactory.create();
405
416
  const startedAt = new Date();
406
417
  const summarizedInput = this.summarizeLlmMessages(messages);
418
+ const { ctx, model, languageModelConnectionNodeId, guardrails } = prepared;
407
419
  const span = this.createModelInvocationSpan(ctx, invocationId, startedAt);
408
- await ctx.nodeState?.markQueued({ nodeId, activationId: ctx.activationId, inputsByPort });
409
- await ctx.nodeState?.markRunning({ nodeId, activationId: ctx.activationId, inputsByPort });
420
+ await ctx.nodeState?.markQueued({
421
+ nodeId: languageModelConnectionNodeId,
422
+ activationId: ctx.activationId,
423
+ inputsByPort: itemInputsByPort,
424
+ });
425
+ await ctx.nodeState?.appendConnectionInvocation({
426
+ invocationId,
427
+ connectionNodeId: languageModelConnectionNodeId,
428
+ parentAgentNodeId: ctx.nodeId,
429
+ parentAgentActivationId: ctx.activationId,
430
+ status: "queued",
431
+ managedInput: summarizedInput,
432
+ queuedAt: startedAt.toISOString(),
433
+ iterationId: ctx.iterationId,
434
+ itemIndex: ctx.itemIndex,
435
+ parentInvocationId: ctx.parentInvocationId,
436
+ });
437
+ await ctx.nodeState?.markRunning({
438
+ nodeId: languageModelConnectionNodeId,
439
+ activationId: ctx.activationId,
440
+ inputsByPort: itemInputsByPort,
441
+ });
442
+ await ctx.nodeState?.appendConnectionInvocation({
443
+ invocationId,
444
+ connectionNodeId: languageModelConnectionNodeId,
445
+ parentAgentNodeId: ctx.nodeId,
446
+ parentAgentActivationId: ctx.activationId,
447
+ status: "running",
448
+ managedInput: summarizedInput,
449
+ queuedAt: startedAt.toISOString(),
450
+ startedAt: startedAt.toISOString(),
451
+ iterationId: ctx.iterationId,
452
+ itemIndex: ctx.itemIndex,
453
+ parentInvocationId: ctx.parentInvocationId,
454
+ });
410
455
  try {
411
- const response = (await model.invoke(messages, options)) as AIMessage;
456
+ const tools = this.buildToolSet(itemScopedTools);
457
+ const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
458
+ const result = await generateText({
459
+ model: model.languageModel as LanguageModel,
460
+ messages: [...messages],
461
+ tools,
462
+ toolChoice: tools ? "auto" : undefined,
463
+ maxOutputTokens: callOptions.maxOutputTokens,
464
+ temperature: callOptions.temperature,
465
+ providerOptions: callOptions.providerOptions as Record<string, Record<string, never>>,
466
+ maxRetries: 0,
467
+ });
468
+ const turnResult = this.extractTurnResult(result as AnyGenerateTextResult);
412
469
  const finishedAt = new Date();
470
+ const managedOutput = this.summarizeTurnOutput(turnResult);
413
471
  await ctx.nodeState?.markCompleted({
414
- nodeId,
472
+ nodeId: languageModelConnectionNodeId,
415
473
  activationId: ctx.activationId,
416
- inputsByPort,
417
- outputs: AgentOutputFactory.fromUnknown({
418
- content: AgentMessageFactory.extractContent(response),
419
- }),
420
- });
421
- const content = AgentMessageFactory.extractContent(response);
422
- await span.attachArtifact({
423
- kind: "ai.messages",
424
- contentType: "application/json",
425
- previewJson: summarizedInput,
474
+ inputsByPort: itemInputsByPort,
475
+ outputs: AgentOutputFactory.fromUnknown(managedOutput),
426
476
  });
427
- await span.attachArtifact({
428
- kind: "ai.response",
429
- contentType: "application/json",
430
- previewJson: content,
431
- });
432
- await this.recordModelUsageMetrics(span, response, ctx);
477
+ await span.attachArtifact({ kind: "ai.messages", contentType: "application/json", previewJson: summarizedInput });
478
+ await span.attachArtifact({ kind: "ai.response", contentType: "application/json", previewJson: turnResult.text });
479
+ await this.recordModelUsageMetrics(span, turnResult.usage, ctx);
433
480
  await span.end({ status: "ok", endedAt: finishedAt });
434
481
  await ctx.nodeState?.appendConnectionInvocation({
435
482
  invocationId,
436
- connectionNodeId: nodeId,
483
+ connectionNodeId: languageModelConnectionNodeId,
437
484
  parentAgentNodeId: ctx.nodeId,
438
485
  parentAgentActivationId: ctx.activationId,
439
486
  status: "completed",
440
487
  managedInput: summarizedInput,
441
- managedOutput: content,
488
+ managedOutput,
442
489
  queuedAt: startedAt.toISOString(),
443
490
  startedAt: startedAt.toISOString(),
444
491
  finishedAt: finishedAt.toISOString(),
492
+ iterationId: ctx.iterationId,
493
+ itemIndex: ctx.itemIndex,
494
+ parentInvocationId: ctx.parentInvocationId,
445
495
  });
446
- return response;
496
+ return turnResult;
447
497
  } catch (error) {
448
498
  await span.end({
449
499
  status: "error",
@@ -454,62 +504,113 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
454
504
  error,
455
505
  invocationId,
456
506
  startedAt,
457
- nodeId,
507
+ nodeId: languageModelConnectionNodeId,
458
508
  ctx,
459
- inputsByPort,
460
- managedInput: this.summarizeLlmMessages(messages),
509
+ inputsByPort: itemInputsByPort,
510
+ managedInput: summarizedInput,
461
511
  });
462
512
  }
463
513
  }
464
514
 
465
- private async invokeStructuredModel(
466
- model: LangChainStructuredOutputModelLike,
467
- nodeId: string,
468
- messages: ReadonlyArray<BaseMessage>,
469
- ctx: NodeExecutionContext<AIAgent<any, any>>,
470
- inputsByPort: NodeInputsByPort,
471
- options?: AgentGuardrailConfig["modelInvocationOptions"],
515
+ /**
516
+ * Structured-output turn: runs `generateText({ output: Output.object({ schema }) })` via the
517
+ * structured-output runner. We keep this as a separate helper because the runner needs the raw
518
+ * validated value (not just text) back, and must be able to retry on Zod failures.
519
+ */
520
+ private async invokeStructuredTurn(
521
+ prepared: PreparedAgentExecution,
522
+ itemInputsByPort: NodeInputsByPort,
523
+ schema: ZodSchemaAny | Readonly<Record<string, unknown>>,
524
+ messages: ReadonlyArray<ModelMessage>,
525
+ structuredOptions: { readonly strict?: boolean; readonly schemaName?: string } | undefined,
472
526
  ): Promise<unknown> {
473
527
  const invocationId = ConnectionInvocationIdFactory.create();
474
528
  const startedAt = new Date();
475
529
  const summarizedInput = this.summarizeLlmMessages(messages);
530
+ const { ctx, model, languageModelConnectionNodeId, guardrails } = prepared;
476
531
  const span = this.createModelInvocationSpan(ctx, invocationId, startedAt);
477
- await ctx.nodeState?.markQueued({ nodeId, activationId: ctx.activationId, inputsByPort });
478
- await ctx.nodeState?.markRunning({ nodeId, activationId: ctx.activationId, inputsByPort });
532
+ await ctx.nodeState?.markQueued({
533
+ nodeId: languageModelConnectionNodeId,
534
+ activationId: ctx.activationId,
535
+ inputsByPort: itemInputsByPort,
536
+ });
537
+ await ctx.nodeState?.appendConnectionInvocation({
538
+ invocationId,
539
+ connectionNodeId: languageModelConnectionNodeId,
540
+ parentAgentNodeId: ctx.nodeId,
541
+ parentAgentActivationId: ctx.activationId,
542
+ status: "queued",
543
+ managedInput: summarizedInput,
544
+ queuedAt: startedAt.toISOString(),
545
+ iterationId: ctx.iterationId,
546
+ itemIndex: ctx.itemIndex,
547
+ parentInvocationId: ctx.parentInvocationId,
548
+ });
549
+ await ctx.nodeState?.markRunning({
550
+ nodeId: languageModelConnectionNodeId,
551
+ activationId: ctx.activationId,
552
+ inputsByPort: itemInputsByPort,
553
+ });
554
+ await ctx.nodeState?.appendConnectionInvocation({
555
+ invocationId,
556
+ connectionNodeId: languageModelConnectionNodeId,
557
+ parentAgentNodeId: ctx.nodeId,
558
+ parentAgentActivationId: ctx.activationId,
559
+ status: "running",
560
+ managedInput: summarizedInput,
561
+ queuedAt: startedAt.toISOString(),
562
+ startedAt: startedAt.toISOString(),
563
+ iterationId: ctx.iterationId,
564
+ itemIndex: ctx.itemIndex,
565
+ parentInvocationId: ctx.parentInvocationId,
566
+ });
479
567
  try {
480
- const response = await model.invoke(messages, options);
568
+ const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
569
+ const outputSchema =
570
+ structuredOptions?.strict && !this.isZodSchema(schema)
571
+ ? Output.object({ schema: jsonSchema(schema as Parameters<typeof jsonSchema>[0]) as never })
572
+ : Output.object({ schema: schema as ZodSchemaAny });
573
+ const result = await generateText({
574
+ model: model.languageModel as LanguageModel,
575
+ messages: [...messages],
576
+ experimental_output: outputSchema,
577
+ maxOutputTokens: callOptions.maxOutputTokens,
578
+ temperature: callOptions.temperature,
579
+ providerOptions: callOptions.providerOptions as Record<string, Record<string, never>>,
580
+ maxRetries: 0,
581
+ });
582
+ const turnResult = this.extractTurnResult(result as AnyGenerateTextResult);
481
583
  const finishedAt = new Date();
482
584
  await ctx.nodeState?.markCompleted({
483
- nodeId,
585
+ nodeId: languageModelConnectionNodeId,
484
586
  activationId: ctx.activationId,
485
- inputsByPort,
486
- outputs: AgentOutputFactory.fromUnknown(response),
487
- });
488
- await span.attachArtifact({
489
- kind: "ai.messages",
490
- contentType: "application/json",
491
- previewJson: summarizedInput,
587
+ inputsByPort: itemInputsByPort,
588
+ outputs: AgentOutputFactory.fromUnknown(result.experimental_output),
492
589
  });
590
+ await span.attachArtifact({ kind: "ai.messages", contentType: "application/json", previewJson: summarizedInput });
493
591
  await span.attachArtifact({
494
592
  kind: "ai.response.structured",
495
593
  contentType: "application/json",
496
- previewJson: this.resultToJsonValue(response),
594
+ previewJson: this.resultToJsonValue(result.experimental_output),
497
595
  });
498
- await this.recordModelUsageMetrics(span, response, ctx);
596
+ await this.recordModelUsageMetrics(span, turnResult.usage, ctx);
499
597
  await span.end({ status: "ok", endedAt: finishedAt });
500
598
  await ctx.nodeState?.appendConnectionInvocation({
501
599
  invocationId,
502
- connectionNodeId: nodeId,
600
+ connectionNodeId: languageModelConnectionNodeId,
503
601
  parentAgentNodeId: ctx.nodeId,
504
602
  parentAgentActivationId: ctx.activationId,
505
603
  status: "completed",
506
604
  managedInput: summarizedInput,
507
- managedOutput: this.resultToJsonValue(response),
605
+ managedOutput: this.resultToJsonValue(result.experimental_output),
508
606
  queuedAt: startedAt.toISOString(),
509
607
  startedAt: startedAt.toISOString(),
510
608
  finishedAt: finishedAt.toISOString(),
609
+ iterationId: ctx.iterationId,
610
+ itemIndex: ctx.itemIndex,
611
+ parentInvocationId: ctx.parentInvocationId,
511
612
  });
512
- return response;
613
+ return result.experimental_output;
513
614
  } catch (error) {
514
615
  await span.end({
515
616
  status: "error",
@@ -520,14 +621,88 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
520
621
  error,
521
622
  invocationId,
522
623
  startedAt,
523
- nodeId,
624
+ nodeId: languageModelConnectionNodeId,
524
625
  ctx,
525
- inputsByPort,
526
- managedInput: this.summarizeLlmMessages(messages),
626
+ inputsByPort: itemInputsByPort,
627
+ managedInput: summarizedInput,
527
628
  });
528
629
  }
529
630
  }
530
631
 
632
+ private isZodSchema(schema: ZodSchemaAny | Readonly<Record<string, unknown>>): schema is ZodSchemaAny {
633
+ const candidate = schema as { parse?: unknown };
634
+ return typeof candidate.parse === "function";
635
+ }
636
+
637
+ private resolveCallOptions(
638
+ model: ChatLanguageModel,
639
+ overrides: AgentGuardrailConfig["modelInvocationOptions"] | undefined,
640
+ ): ChatLanguageModelCallOptions {
641
+ const defaults = model.defaultCallOptions ?? {};
642
+ return {
643
+ maxOutputTokens: overrides?.maxTokens ?? defaults.maxOutputTokens,
644
+ temperature: defaults.temperature,
645
+ providerOptions: (overrides?.providerOptions ??
646
+ defaults.providerOptions) as ChatLanguageModelCallOptions["providerOptions"],
647
+ };
648
+ }
649
+
650
+ /**
651
+ * Build a no-code-friendly output payload for an LLM round.
652
+ *
653
+ * Always includes `content` (matching the canvas snapshot shape used elsewhere) and adds a
654
+ * `toolCalls` array when the round produced tool calls so the execution inspector surfaces the
655
+ * planned calls instead of just an empty `""` for tool-only rounds.
656
+ */
657
+ private summarizeTurnOutput(turnResult: TurnResult): JsonValue {
658
+ if (turnResult.toolCalls.length === 0) return { content: turnResult.text };
659
+ const toolCalls = turnResult.toolCalls.map((toolCall) => ({
660
+ name: toolCall.name,
661
+ args: this.resultToJsonValue(toolCall.input) ?? null,
662
+ }));
663
+ return { content: turnResult.text, toolCalls };
664
+ }
665
+
666
+ private extractTurnResult(result: AnyGenerateTextResult): TurnResult {
667
+ const usage = this.extractUsageFromResult(result);
668
+ const text = result.text;
669
+ const toolCalls: ReadonlyArray<AgentToolCall> = result.toolCalls.map((toolCall) => ({
670
+ id: toolCall.toolCallId,
671
+ name: toolCall.toolName,
672
+ input: (toolCall as { input?: unknown }).input,
673
+ }));
674
+ const assistantMessage = this.extractAssistantMessage(result);
675
+ return {
676
+ assistantMessage,
677
+ text,
678
+ toolCalls,
679
+ usage,
680
+ };
681
+ }
682
+
683
+ private extractAssistantMessage(result: AnyGenerateTextResult): AssistantModelMessage | undefined {
684
+ const responseMessages: ReadonlyArray<ModelMessage> = result.response.messages;
685
+ const assistantMessages = responseMessages.filter((m) => m.role === "assistant");
686
+ return assistantMessages[assistantMessages.length - 1];
687
+ }
688
+
689
+ private extractUsageFromResult(result: AnyGenerateTextResult): ModelUsage {
690
+ const usage = result.usage;
691
+ const inputTokens = this.toFiniteNumber(usage.inputTokens);
692
+ const outputTokens = this.toFiniteNumber(usage.outputTokens);
693
+ const totalTokens =
694
+ this.toFiniteNumber(usage.totalTokens) ??
695
+ (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
696
+ const cachedInputTokens = this.toFiniteNumber(usage.cachedInputTokens);
697
+ const reasoningTokens = this.toFiniteNumber(usage.reasoningTokens);
698
+ return { inputTokens, outputTokens, totalTokens, cachedInputTokens, reasoningTokens };
699
+ }
700
+
701
+ private toFiniteNumber(value: unknown): number | undefined {
702
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
703
+ return value;
704
+ }
705
+
531
706
  private createModelInvocationSpan(
532
707
  ctx: NodeExecutionContext<AIAgent<any, any>>,
533
708
  invocationId: string,
@@ -541,20 +716,31 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
541
716
  [CodemationTelemetryAttributeNames.connectionInvocationId]: invocationId,
542
717
  [GenAiTelemetryAttributeNames.operationName]: "chat",
543
718
  [GenAiTelemetryAttributeNames.requestModel]: this.resolveChatModelName(ctx.config.chatModel),
719
+ ...(ctx.iterationId ? { [CodemationTelemetryAttributeNames.iterationId]: ctx.iterationId } : {}),
720
+ ...(typeof ctx.itemIndex === "number"
721
+ ? { [CodemationTelemetryAttributeNames.iterationIndex]: ctx.itemIndex }
722
+ : {}),
723
+ ...(ctx.parentInvocationId
724
+ ? { [CodemationTelemetryAttributeNames.parentInvocationId]: ctx.parentInvocationId }
725
+ : {}),
544
726
  },
545
727
  });
546
728
  }
547
729
 
548
730
  private async recordModelUsageMetrics(
549
731
  span: ReturnType<NodeExecutionContext["telemetry"]["startChildSpan"]>,
550
- response: unknown,
732
+ usage: ModelUsage,
551
733
  ctx: NodeExecutionContext<AIAgent<any, any>>,
552
734
  ) {
553
- const usage = this.extractModelUsageMetrics(response);
554
- for (const [name, value] of Object.entries(usage)) {
555
- if (value === undefined) {
556
- continue;
557
- }
735
+ const entries: ReadonlyArray<readonly [string, number | undefined]> = [
736
+ [GenAiTelemetryAttributeNames.usageInputTokens, usage.inputTokens],
737
+ [GenAiTelemetryAttributeNames.usageOutputTokens, usage.outputTokens],
738
+ [GenAiTelemetryAttributeNames.usageTotalTokens, usage.totalTokens],
739
+ [GenAiTelemetryAttributeNames.usageCacheReadInputTokens, usage.cachedInputTokens],
740
+ [GenAiTelemetryAttributeNames.usageReasoningTokens, usage.reasoningTokens],
741
+ ];
742
+ for (const [name, value] of entries) {
743
+ if (value === undefined) continue;
558
744
  await span.recordMetric({ name, value });
559
745
  }
560
746
  await this.captureCostTrackingUsage(span, ctx, usage);
@@ -563,38 +749,32 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
563
749
  private async captureCostTrackingUsage(
564
750
  span: ReturnType<NodeExecutionContext["telemetry"]["startChildSpan"]>,
565
751
  ctx: NodeExecutionContext<AIAgent<any, any>>,
566
- usage: Readonly<Record<string, number | undefined>>,
752
+ usage: ModelUsage,
567
753
  ): Promise<void> {
568
754
  const costTracking = span.costTracking;
569
- if (!costTracking) {
570
- return;
571
- }
755
+ if (!costTracking) return;
572
756
  const provider = ctx.config.chatModel.provider;
573
757
  const pricingKey = ctx.config.chatModel.modelName;
574
- if (!provider || !pricingKey) {
575
- return;
576
- }
577
- const inputTokens = usage[GenAiTelemetryAttributeNames.usageInputTokens];
578
- const outputTokens = usage[GenAiTelemetryAttributeNames.usageOutputTokens];
579
- if (inputTokens !== undefined) {
758
+ if (!provider || !pricingKey) return;
759
+ if (usage.inputTokens !== undefined) {
580
760
  await costTracking.captureUsage({
581
761
  component: "chat",
582
762
  provider,
583
763
  operation: "completion.input",
584
764
  pricingKey,
585
765
  usageUnit: "input_tokens",
586
- quantity: inputTokens,
766
+ quantity: usage.inputTokens,
587
767
  modelName: pricingKey,
588
768
  });
589
769
  }
590
- if (outputTokens !== undefined) {
770
+ if (usage.outputTokens !== undefined) {
591
771
  await costTracking.captureUsage({
592
772
  component: "chat",
593
773
  provider,
594
774
  operation: "completion.output",
595
775
  pricingKey,
596
776
  usageUnit: "output_tokens",
597
- quantity: outputTokens,
777
+ quantity: usage.outputTokens,
598
778
  modelName: pricingKey,
599
779
  });
600
780
  }
@@ -604,100 +784,29 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
604
784
  return chatModel.modelName ?? chatModel.name;
605
785
  }
606
786
 
607
- private extractModelUsageMetrics(response: unknown): Readonly<Record<string, number | undefined>> {
608
- const usage = this.extractUsageObject(response);
609
- const inputTokens = this.readUsageNumber(usage, ["input_tokens", "inputTokens", "prompt_tokens", "promptTokens"]);
610
- const outputTokens = this.readUsageNumber(usage, [
611
- "output_tokens",
612
- "outputTokens",
613
- "completion_tokens",
614
- "completionTokens",
615
- ]);
616
- const totalTokens =
617
- this.readUsageNumber(usage, ["total_tokens", "totalTokens"]) ??
618
- (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
619
- const cachedInputTokens = this.readUsageNumber(usage, [
620
- "cache_read_input_tokens",
621
- "cacheReadInputTokens",
622
- "input_token_details.cached_tokens",
623
- ]);
624
- const reasoningTokens = this.readUsageNumber(usage, [
625
- "reasoning_tokens",
626
- "reasoningTokens",
627
- "output_token_details.reasoning_tokens",
628
- ]);
629
- return {
630
- [GenAiTelemetryAttributeNames.usageInputTokens]: inputTokens,
631
- [GenAiTelemetryAttributeNames.usageOutputTokens]: outputTokens,
632
- [GenAiTelemetryAttributeNames.usageTotalTokens]: totalTokens,
633
- [GenAiTelemetryAttributeNames.usageCacheReadInputTokens]: cachedInputTokens,
634
- [GenAiTelemetryAttributeNames.usageReasoningTokens]: reasoningTokens,
635
- };
636
- }
637
-
638
- private extractUsageObject(response: unknown): Readonly<Record<string, unknown>> | undefined {
639
- if (!this.isRecord(response)) {
640
- return undefined;
641
- }
642
- const usageMetadata = response["usage_metadata"];
643
- if (this.isRecord(usageMetadata)) {
644
- return usageMetadata;
645
- }
646
- const responseMetadata = response["response_metadata"];
647
- if (this.isRecord(responseMetadata)) {
648
- const tokenUsage = responseMetadata["tokenUsage"];
649
- if (this.isRecord(tokenUsage)) {
650
- return tokenUsage;
651
- }
652
- const usage = responseMetadata["usage"];
653
- if (this.isRecord(usage)) {
654
- return usage;
655
- }
656
- }
657
- return undefined;
658
- }
659
-
660
- private readUsageNumber(
661
- source: Readonly<Record<string, unknown>> | undefined,
662
- keys: ReadonlyArray<string>,
663
- ): number | undefined {
664
- for (const key of keys) {
665
- const value = this.readNestedUsageValue(source, key);
666
- if (typeof value === "number" && Number.isFinite(value)) {
667
- return value;
668
- }
669
- }
670
- return undefined;
671
- }
672
-
673
- private readNestedUsageValue(source: Readonly<Record<string, unknown>> | undefined, dottedKey: string): unknown {
674
- if (!source) {
675
- return undefined;
676
- }
677
- let current: unknown = source;
678
- for (const segment of dottedKey.split(".")) {
679
- if (!this.isRecord(current)) {
680
- return undefined;
681
- }
682
- current = current[segment];
683
- }
684
- return current;
685
- }
686
-
687
- private isRecord(value: unknown): value is Record<string, unknown> {
688
- return typeof value === "object" && value !== null;
689
- }
690
-
691
787
  private async markQueuedTools(
692
788
  plannedToolCalls: ReadonlyArray<PlannedToolCall>,
693
789
  ctx: NodeExecutionContext<AIAgent<any, any>>,
694
790
  ): Promise<void> {
791
+ const queuedAt = new Date().toISOString();
695
792
  for (const plannedToolCall of plannedToolCalls) {
696
793
  await ctx.nodeState?.markQueued({
697
794
  nodeId: plannedToolCall.nodeId,
698
795
  activationId: ctx.activationId,
699
796
  inputsByPort: AgentToolCallPortMap.fromInput(plannedToolCall.toolCall.input ?? {}),
700
797
  });
798
+ await ctx.nodeState?.appendConnectionInvocation({
799
+ invocationId: plannedToolCall.invocationId,
800
+ connectionNodeId: plannedToolCall.nodeId,
801
+ parentAgentNodeId: ctx.nodeId,
802
+ parentAgentActivationId: ctx.activationId,
803
+ status: "queued",
804
+ managedInput: this.resultToJsonValue(plannedToolCall.toolCall.input),
805
+ queuedAt,
806
+ iterationId: ctx.iterationId,
807
+ itemIndex: ctx.itemIndex,
808
+ parentInvocationId: ctx.parentInvocationId,
809
+ });
701
810
  }
702
811
  }
703
812
 
@@ -717,6 +826,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
717
826
  toolCall,
718
827
  invocationIndex,
719
828
  nodeId: ConnectionNodeIdFactory.toolConnectionNodeId(parentNodeId, binding.config.name),
829
+ invocationId: ConnectionInvocationIdFactory.create(),
720
830
  } satisfies PlannedToolCall;
721
831
  });
722
832
  }
@@ -756,11 +866,14 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
756
866
  queuedAt: args.startedAt.toISOString(),
757
867
  startedAt: args.startedAt.toISOString(),
758
868
  finishedAt: finishedAt.toISOString(),
869
+ iterationId: args.ctx.iterationId,
870
+ itemIndex: args.ctx.itemIndex,
871
+ parentInvocationId: args.ctx.parentInvocationId,
759
872
  });
760
873
  return effectiveError;
761
874
  }
762
875
 
763
- private summarizeLlmMessages(messages: ReadonlyArray<BaseMessage>): JsonValue {
876
+ private summarizeLlmMessages(messages: ReadonlyArray<ModelMessage>): JsonValue {
764
877
  const last = messages[messages.length - 1];
765
878
  const preview =
766
879
  typeof last?.content === "string"
@@ -775,9 +888,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
775
888
  }
776
889
 
777
890
  private resultToJsonValue(value: unknown): JsonValue | undefined {
778
- if (value === undefined) {
779
- return undefined;
780
- }
891
+ if (value === undefined) return undefined;
781
892
  const json = JSON.stringify(value);
782
893
  return JSON.parse(json) as JsonValue;
783
894
  }
@@ -787,7 +898,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
787
898
  itemIndex: number,
788
899
  items: Items,
789
900
  ctx: NodeExecutionContext<AIAgent<any, any>>,
790
- ): ReadonlyArray<BaseMessage> {
901
+ ): ReadonlyArray<ModelMessage> {
791
902
  return AgentMessageFactory.createPromptMessages(
792
903
  AgentMessageConfigNormalizer.resolveFromInputOrConfig(item.json, ctx.config, {
793
904
  item,
@@ -803,7 +914,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
803
914
  const inputSchema = config.getInputSchema();
804
915
  if (inputSchema == null) {
805
916
  throw new Error(
806
- `AIAgent tool "${config.name}": node-backed tool is missing inputSchema (cannot build LangChain tool).`,
917
+ `AIAgent tool "${config.name}": node-backed tool is missing inputSchema (cannot build AI SDK tool).`,
807
918
  );
808
919
  }
809
920
  return {
@@ -816,7 +927,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
816
927
  const inputSchema = config.getInputSchema();
817
928
  if (inputSchema == null) {
818
929
  throw new Error(
819
- `AIAgent tool "${config.name}": callable tool is missing inputSchema (cannot build LangChain tool).`,
930
+ `AIAgent tool "${config.name}": callable tool is missing inputSchema (cannot build AI SDK tool).`,
820
931
  );
821
932
  }
822
933
  return {
@@ -837,11 +948,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
837
948
  };
838
949
  }
839
950
 
840
- /**
841
- * Consumer apps can resolve two copies of `@codemation/core`, breaking `instanceof NodeBackedToolConfig` and
842
- * sending node-backed tools down the plugin-tool branch with `inputSchema: undefined` (LangChain then crashes in
843
- * json-schema validation). {@link NodeBackedToolConfig#toolKind} is stable across copies.
844
- */
845
951
  private isNodeBackedToolConfig(config: ToolConfig): config is NodeBackedToolConfig<any, any, any> {
846
952
  return (
847
953
  config instanceof NodeBackedToolConfig ||
@@ -849,9 +955,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
849
955
  );
850
956
  }
851
957
 
852
- /**
853
- * Callable tools use {@link CallableToolConfig#toolKind} for cross-package / JSON round-trip safety.
854
- */
855
958
  private isCallableToolConfig(config: ToolConfig): config is CallableToolConfig<ZodSchemaAny, ZodSchemaAny> {
856
959
  return (
857
960
  config instanceof CallableToolConfig ||