@codemation/core-nodes 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +222 -0
- package/dist/index.cjs +3485 -474
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1763 -685
- package/dist/index.d.ts +1763 -685
- package/dist/index.js +3452 -479
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/src/authoring/defineRestNode.types.ts +204 -0
- package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
- package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
- package/src/credentials/ApiKeyCredentialType.ts +60 -0
- package/src/credentials/BasicAuthCredentialType.ts +51 -0
- package/src/credentials/BearerTokenCredentialType.ts +40 -0
- package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
- package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
- package/src/credentials/index.ts +4 -0
- package/src/http/HttpBodyBuilder.ts +90 -0
- package/src/http/HttpRequestExecutor.ts +150 -0
- package/src/http/HttpUrlBuilder.ts +22 -0
- package/src/http/httpRequest.types.ts +69 -0
- package/src/index.ts +10 -1
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
- package/src/nodes/AIAgentNode.ts +391 -288
- package/src/nodes/AgentMessageFactory.ts +57 -49
- package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
- package/src/nodes/AgentToolExecutionCoordinator.ts +31 -16
- package/src/nodes/AssertionNode.ts +42 -0
- package/src/nodes/CronTriggerFactory.ts +45 -0
- package/src/nodes/CronTriggerNode.ts +40 -0
- package/src/nodes/HttpRequestNodeFactory.ts +99 -23
- package/src/nodes/IsTestRunNode.ts +25 -0
- package/src/nodes/NodeBackedToolRuntime.ts +40 -4
- package/src/nodes/TestTriggerNode.ts +33 -0
- package/src/nodes/WebhookTriggerFactory.ts +1 -1
- package/src/nodes/aggregate.ts +1 -1
- package/src/nodes/aiAgentSupport.types.ts +22 -2
- package/src/nodes/assertion.ts +42 -0
- package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
- package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
- package/src/nodes/collections/collectionGetNode.types.ts +26 -0
- package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
- package/src/nodes/collections/collectionListNode.types.ts +30 -0
- package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
- package/src/nodes/collections/index.ts +6 -0
- package/src/nodes/httpRequest.ts +62 -1
- package/src/nodes/if.ts +1 -1
- package/src/nodes/isTestRun.ts +24 -0
- package/src/nodes/mapData.ts +1 -0
- package/src/nodes/merge.ts +1 -1
- package/src/nodes/noOp.ts +1 -0
- package/src/nodes/split.ts +1 -1
- package/src/nodes/testTrigger.ts +72 -0
- package/src/nodes/wait.ts +1 -0
- package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
170
|
-
|
|
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.
|
|
203
|
+
loopResult.finalText,
|
|
209
204
|
itemScopedTools.length > 0,
|
|
210
205
|
);
|
|
211
206
|
return this.buildOutputItem(item, outputJson);
|
|
212
207
|
}
|
|
213
208
|
|
|
214
209
|
/**
|
|
215
|
-
*
|
|
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:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const {
|
|
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
|
|
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
|
|
235
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
294
|
-
assistantMessage:
|
|
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
|
-
|
|
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<
|
|
309
|
-
|
|
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(
|
|
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:
|
|
320
|
-
|
|
316
|
+
conversation: conversationWithFinal,
|
|
317
|
+
rawFinalText: finalText,
|
|
321
318
|
agentName: this.getAgentDisplayName(prepared.ctx),
|
|
322
319
|
nodeId: prepared.ctx.nodeId,
|
|
323
|
-
invokeTextModel: async (messages) =>
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
385
|
-
entry,
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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({
|
|
409
|
-
|
|
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
|
|
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
|
-
|
|
429
|
-
|
|
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:
|
|
483
|
+
connectionNodeId: languageModelConnectionNodeId,
|
|
437
484
|
parentAgentNodeId: ctx.nodeId,
|
|
438
485
|
parentAgentActivationId: ctx.activationId,
|
|
439
486
|
status: "completed",
|
|
440
487
|
managedInput: summarizedInput,
|
|
441
|
-
managedOutput
|
|
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
|
|
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:
|
|
509
|
+
inputsByPort: itemInputsByPort,
|
|
510
|
+
managedInput: summarizedInput,
|
|
461
511
|
});
|
|
462
512
|
}
|
|
463
513
|
}
|
|
464
514
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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({
|
|
478
|
-
|
|
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
|
|
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(
|
|
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(
|
|
594
|
+
previewJson: this.resultToJsonValue(result.experimental_output),
|
|
497
595
|
});
|
|
498
|
-
await this.recordModelUsageMetrics(span,
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
732
|
+
usage: ModelUsage,
|
|
551
733
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
552
734
|
) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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:
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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
|
|
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
|
|
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 ||
|